├── Cafe.Matcha
├── Constant
│ ├── .gitignore
│ ├── Region.cs
│ ├── FishEventBiteType.cs
│ ├── FishEventType.cs
│ ├── RequestType.cs
│ ├── TreasureShiftingWheelResultType.cs
│ ├── Secret.cs
│ ├── ActorControlType.cs
│ ├── LogType.cs
│ ├── EventType.cs
│ └── MatchaOpcode.cs
├── GitHub-Mark-32px.png
├── FodyWeavers.xml
├── Models
│ ├── DynamicEventData.cs
│ ├── FateData.cs
│ ├── WorldData.cs
│ ├── Template.cs
│ ├── InstanceData.cs
│ ├── ItemName.cs
│ └── TelemetryData.cs
├── Network
│ ├── Structures
│ │ ├── Materia.cs
│ │ ├── IMarketBoardPurchase.cs
│ │ ├── WorldVisitQueue.cs
│ │ ├── IMarketBoardPurchaseHandler.cs
│ │ ├── MarketBoardPurchase.cs
│ │ ├── IMarketBoardHistory.cs
│ │ ├── IMarketTaxRates.cs
│ │ ├── MarketBoardItemRequest.cs
│ │ ├── Examine.cs
│ │ ├── MarketBoardPurchaseHandler.cs
│ │ ├── IMarketBoardCurrentOfferings.cs
│ │ ├── MarketTaxRates.cs
│ │ └── MarketBoardHistory.cs
│ ├── Handler
│ │ ├── AbstractHandler.cs
│ │ ├── QueueHandler.cs
│ │ ├── FishingHandler.cs
│ │ └── MarketBoardHandler.cs
│ ├── Universalis
│ │ ├── Types
│ │ │ ├── UniversalisItemMateria.cs
│ │ │ ├── UniversalisQueryResponse.cs
│ │ │ ├── UniversalisTaxUploadRequest.cs
│ │ │ ├── UniversalisItemListingsUploadRequest.cs
│ │ │ ├── UniversalisItemUploadRequest.cs
│ │ │ ├── UniversalisItemListingDeleteRequest.cs
│ │ │ ├── UniversalisTaxData.cs
│ │ │ ├── UniversalisHistoryEntry.cs
│ │ │ └── UniversalisItemListingsEntry.cs
│ │ ├── Client.cs
│ │ └── Api.cs
│ ├── Packet.cs
│ └── State.cs
├── DTO
│ ├── BaseDTO.cs
│ ├── FishBiteDTO.cs
│ ├── InitZoneDTO.cs
│ ├── MatchAlertDTO.cs
│ ├── TreasureResultDTO.cs
│ ├── FateWatchListChangedDTO.cs
│ ├── TreasureSpotDTO.cs
│ ├── FateDTO.cs
│ ├── MiniCactpotDTO.cs
│ ├── QueueDTO.cs
│ ├── MarketBoardItemListingCountDTO.cs
│ ├── DynamicEventDTO.cs
│ ├── MarketBoardItemListingDTO.cs
│ ├── GearsetDTO.cs
│ └── CompanyVoyageStatusDTO.cs
├── GlobalSuppressions.cs
├── Utils
│ ├── FateManager.cs
│ ├── BindingTarget.cs
│ ├── Log.cs
│ ├── ParsePlugin.cs
│ ├── Telemetry.cs
│ ├── Helper.cs
│ ├── Output.cs
│ └── Request.cs
├── data
│ ├── patch.json
│ ├── template.json
│ ├── type.json
│ └── roulette.json
├── ViewModels
│ └── TelemetrySetting.cs
├── Views
│ ├── License.xaml.cs
│ ├── OverrideConfirm.xaml.cs
│ ├── OverrideConfirm.xaml
│ ├── License.xaml
│ ├── TelemetrySetting.xaml.cs
│ └── TelemetrySetting.xaml
├── Cafe.Matcha.crproj
├── Config.cs
├── MatchaInit.cs
├── Data.cs
├── Telemetry
│ ├── Fate.cs
│ └── Npc.cs
└── Cafe.Matcha.csproj
├── utils
├── lib
│ ├── opcode.mjs
│ └── csv.mjs
├── package.json
└── update-opcode.mjs
├── README.zh_CN.md
├── Cafe.Matcha.Packer
├── Cafe.Matcha.Packer.csproj
└── Program.cs
├── Directory.Build.props
├── stylecop.json
├── README.md
├── Cafe.Matcha.sln
├── .github
└── workflows
│ └── build.yml
├── StyleCop.ruleset
└── .gitignore
/Cafe.Matcha/Constant/.gitignore:
--------------------------------------------------------------------------------
1 | Secret.Local.cs
--------------------------------------------------------------------------------
/Cafe.Matcha/GitHub-Mark-32px.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/thewakingsands/matcha/HEAD/Cafe.Matcha/GitHub-Mark-32px.png
--------------------------------------------------------------------------------
/utils/lib/opcode.mjs:
--------------------------------------------------------------------------------
1 | export function formatOpcode(num) {
2 | return num.toString(16).padStart(4, '0')
3 | }
4 |
5 | export function parseOpcode(text) {
6 | return parseInt(text, 16)
7 | }
8 |
--------------------------------------------------------------------------------
/utils/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "dependencies": {
3 | "csv-parse": "^5.3.0"
4 | },
5 | "prettier": {
6 | "singleQuote": true,
7 | "semi": false,
8 | "trailingComma": "none"
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/Cafe.Matcha/FodyWeavers.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/README.zh_CN.md:
--------------------------------------------------------------------------------
1 | # Cafe.Matcha
2 |
3 | 抹茶 (Matcha) 是一个提供 F.A.T.E.、副本匹配、区域切换等游戏内消息提醒的 ACT 插件。和 [抹茶悬浮窗](https://github.com/zhyupe/matcha-overlay) 一同使用时,同时具有市场版查询、装备比较、宝图定位、仙人微彩预测等功能。
4 |
5 | 抹茶 (Matcha) 的名称来源于 Match'a(Match 为匹配、比赛)。本项目起初专注于匹配相关内容的提示。
6 |
7 | 目前抹茶仅提供中文界面,且仅对国服提供更新支持。在更新相关 opcode 后本插件应当能在国际服正常工作。同时本插件欢迎国际化相关的代码贡献。
8 |
--------------------------------------------------------------------------------
/Cafe.Matcha/Constant/Region.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) FFCafe. All rights reserved.
2 | // Licensed under the AGPL-3.0 license. See LICENSE file in the project root for full license information.
3 |
4 | namespace Cafe.Matcha.Constant
5 | {
6 | public enum Region
7 | {
8 | Global,
9 | China,
10 | Korea
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/Cafe.Matcha/Constant/FishEventBiteType.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) FFCafe. All rights reserved.
2 | // Licensed under the AGPL-3.0 license. See LICENSE file in the project root for full license information.
3 |
4 | namespace Cafe.Matcha.Constant
5 | {
6 | internal enum FishEventBiteType
7 | {
8 | Light = 292,
9 | Medium = 293,
10 | Big = 294
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/Cafe.Matcha.Packer/Cafe.Matcha.Packer.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 | net8.0
4 | Exe
5 | true
6 | ..\bin\
7 | false
8 |
9 |
--------------------------------------------------------------------------------
/Cafe.Matcha/Constant/FishEventType.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) FFCafe. All rights reserved.
2 | // Licensed under the AGPL-3.0 license. See LICENSE file in the project root for full license information.
3 |
4 | namespace Cafe.Matcha.Constant
5 | {
6 | internal enum FishEventType
7 | {
8 | Cast = 1,
9 | Hook = 2,
10 | End = 3,
11 | Bite = 5
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/Cafe.Matcha/Constant/RequestType.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) FFCafe. All rights reserved.
2 | // Licensed under the AGPL-3.0 license. See LICENSE file in the project root for full license information.
3 |
4 | namespace Cafe.Matcha.Constant
5 | {
6 | public enum RequestType
7 | {
8 | Get = 1,
9 | Form = 2,
10 | JSON = 3,
11 | Multipart = 4
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/Cafe.Matcha/Models/DynamicEventData.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) FFCafe. All rights reserved.
2 | // Licensed under the AGPL-3.0 license. See LICENSE file in the project root for full license information.
3 |
4 | namespace Cafe.Matcha.Models
5 | {
6 | using Newtonsoft.Json;
7 |
8 | public class DynamicEventData
9 | {
10 | [JsonProperty("name")]
11 | public ItemName Name;
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/Directory.Build.props:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | $(SolutionDir)StyleCop.ruleset
5 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/Cafe.Matcha/Constant/TreasureShiftingWheelResultType.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) FFCafe. All rights reserved.
2 | // Licensed under the AGPL-3.0 license. See LICENSE file in the project root for full license information.
3 |
4 | namespace Cafe.Matcha.Constant
5 | {
6 | internal enum TreasureShiftingWheelResultType
7 | {
8 | Low = 191,
9 | Medium = 192,
10 | High = 193,
11 | Shift = 194,
12 | Special = 195,
13 | End = 196
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/Cafe.Matcha/Network/Structures/Materia.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) FFCafe. All rights reserved.
2 | // Licensed under the AGPL-3.0 license. See LICENSE file in the project root for full license information.
3 |
4 | namespace Cafe.Matcha.Network.Structures
5 | {
6 | using Newtonsoft.Json;
7 | public class Materia
8 | {
9 | [JsonProperty("type")]
10 | public int Type { get; internal set; }
11 |
12 | [JsonProperty("tier")]
13 | public int Tier { get; internal set; }
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/utils/lib/csv.mjs:
--------------------------------------------------------------------------------
1 | import { parse } from 'csv-parse/sync'
2 |
3 | export function readCsv(input, fields, { header = 1, skip = 3 } = {}) {
4 | const lines = parse(input, {
5 | skip_empty_lines: true
6 | })
7 |
8 | if (!Array.isArray(fields)) {
9 | fields = lines[header]
10 | }
11 |
12 | return lines.slice(skip).map(line => fields.reduce((obj, field, i) => {
13 | obj[`$${i}`] = line[i]
14 | if (field) {
15 | obj[field] = line[i]
16 | }
17 | return obj
18 | }, { _: line }))
19 | }
--------------------------------------------------------------------------------
/Cafe.Matcha/Constant/Secret.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) FFCafe. All rights reserved.
2 | // Licensed under the AGPL-3.0 license. See LICENSE file in the project root for full license information.
3 |
4 | namespace Cafe.Matcha.Constant
5 | {
6 | internal static partial class Secret
7 | {
8 | public static string TelemetryRoot = "";
9 | public static string UniversalisKey = "";
10 |
11 | public static string TelemetryFate = "";
12 | public static string TelemetryNpc = "";
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/Cafe.Matcha/Constant/ActorControlType.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) FFCafe. All rights reserved.
2 | // Licensed under the AGPL-3.0 license. See LICENSE file in the project root for full license information.
3 |
4 | namespace Cafe.Matcha.Constant
5 | {
6 | internal enum ActorControlType : ushort
7 | {
8 | SetStatus = 2,
9 | DefeatMsg = 6,
10 | TreasureSpot = 84,
11 | DirectorUpdate = 109,
12 | FishingBaitChange = 325,
13 | FateStart = 2370,
14 | FateEnd = 2357,
15 | FateProgress = 2364
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/Cafe.Matcha/Constant/LogType.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) FFCafe. All rights reserved.
2 | // Licensed under the AGPL-3.0 license. See LICENSE file in the project root for full license information.
3 |
4 | namespace Cafe.Matcha.Constant
5 | {
6 | public enum LogType
7 | {
8 | None,
9 | Universalis,
10 | LogLine,
11 | State,
12 | Event,
13 |
14 | #if DEBUG
15 | Request,
16 | Telemetry,
17 | ActorControlSelf,
18 | InvalidPacket,
19 | RawPacket,
20 | Debug1,
21 | #endif
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/Cafe.Matcha/DTO/BaseDTO.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) FFCafe. All rights reserved.
2 | // Licensed under the AGPL-3.0 license. See LICENSE file in the project root for full license information.
3 |
4 | namespace Cafe.Matcha.DTO
5 | {
6 | using Cafe.Matcha.Constant;
7 | using Newtonsoft.Json;
8 |
9 | internal abstract class BaseDTO
10 | {
11 | [JsonIgnore]
12 | public abstract EventType EventType { get; }
13 |
14 | public virtual string ToJSON()
15 | {
16 | return JsonConvert.SerializeObject(this);
17 | }
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/Cafe.Matcha/Models/FateData.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) FFCafe. All rights reserved.
2 | // Licensed under the AGPL-3.0 license. See LICENSE file in the project root for full license information.
3 |
4 | namespace Cafe.Matcha.Models
5 | {
6 | using Newtonsoft.Json;
7 |
8 | public class FateData
9 | {
10 | [JsonProperty("name")]
11 | public ItemName Name;
12 |
13 | [JsonProperty("level")]
14 | public int Level;
15 |
16 | [JsonProperty("patch")]
17 | public int Patch;
18 |
19 | [JsonProperty("location")]
20 | public int Location;
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/stylecop.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://raw.githubusercontent.com/DotNetAnalyzers/StyleCopAnalyzers/master/StyleCop.Analyzers/StyleCop.Analyzers/Settings/stylecop.schema.json",
3 | "settings": {
4 | "documentationRules": {
5 | "companyName": "FFCafe",
6 | "copyrightText": "Copyright (c) {companyName}. All rights reserved.\nLicensed under the {licenseName} license. See {licenseFile} file in the project root for full license information.",
7 | "xmlHeader": false,
8 | "variables": {
9 | "licenseName": "AGPL-3.0",
10 | "licenseFile": "LICENSE"
11 | }
12 | }
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/Cafe.Matcha/Network/Handler/AbstractHandler.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) FFCafe. All rights reserved.
2 | // Licensed under the AGPL-3.0 license. See LICENSE file in the project root for full license information.
3 |
4 | namespace Cafe.Matcha.Network.Handler
5 | {
6 | using System;
7 | using Cafe.Matcha.DTO;
8 |
9 | internal abstract class AbstractHandler
10 | {
11 | protected Action fireEvent;
12 | protected AbstractHandler(Action fireEvent)
13 | {
14 | this.fireEvent = fireEvent;
15 | }
16 |
17 | public abstract bool Handle(Packet packet);
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/Cafe.Matcha/Network/Universalis/Types/UniversalisItemMateria.cs:
--------------------------------------------------------------------------------
1 | namespace Cafe.Matcha.Network.Universalis
2 | {
3 | using Newtonsoft.Json;
4 |
5 | ///
6 | /// A Universalis API structure.
7 | ///
8 | internal class UniversalisItemMateria
9 | {
10 | ///
11 | /// Gets or sets the item slot ID.
12 | ///
13 | [JsonProperty("slotID")]
14 | public int SlotId { get; set; }
15 |
16 | ///
17 | /// Gets or sets the materia ID.
18 | ///
19 | [JsonProperty("materiaID")]
20 | public int MateriaId { get; set; }
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Cafe.Matcha
2 |
3 | Matcha (抹茶) is an ACT plugin that provides notifications for in-game events such as F.A.T.E.s, duty notifications and zone changing. It also has functions like market board querying, gearsets comparison, treasure map locating, and cactpot predictions when it's used together with [Matcha Overlay](https://github.com/zhyupe/matcha-overlay).
4 |
5 | The name of Matcha is from Match'a, with the project initially focusing on matching events.
6 |
7 | At this time, Matcha only provides up-to-date support for the CN server and only available in Chinese. It should be working in the Global server with proper opcodes filled, and I18n related contributions are welcomed.
--------------------------------------------------------------------------------
/Cafe.Matcha/Network/Universalis/Types/UniversalisQueryResponse.cs:
--------------------------------------------------------------------------------
1 | namespace Cafe.Matcha.Network.Universalis
2 | {
3 | using Newtonsoft.Json;
4 |
5 | internal class UniversalisItem
6 | {
7 | [JsonProperty("pricePerUnit")]
8 | public int PricePerUint;
9 |
10 | [JsonProperty("worldName")]
11 | public string WorldName;
12 |
13 | [JsonProperty("quantity")]
14 | public int Quantity;
15 |
16 | [JsonProperty("hq")]
17 | public bool Hq;
18 | }
19 |
20 | internal class UniversalisQueryResponse
21 | {
22 | [JsonProperty("listings")]
23 | public UniversalisItem[] ListingItems;
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/Cafe.Matcha/DTO/FishBiteDTO.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) FFCafe. All rights reserved.
2 | // Licensed under the AGPL-3.0 license. See LICENSE file in the project root for full license information.
3 |
4 | namespace Cafe.Matcha.DTO
5 | {
6 | using Cafe.Matcha.Constant;
7 | using Newtonsoft.Json;
8 |
9 | internal class FishBiteDTO : BaseDTO
10 | {
11 | public override EventType EventType
12 | {
13 | get
14 | {
15 | return EventType.FishBite;
16 | }
17 | }
18 |
19 | [JsonProperty("type")]
20 | public int Type;
21 |
22 | [JsonProperty("time")]
23 | public long Time;
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/Cafe.Matcha/GlobalSuppressions.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) FFCafe. All rights reserved.
2 | // Licensed under the AGPL-3.0 license. See LICENSE file in the project root for full license information.
3 |
4 | // This file is used by Code Analysis to maintain SuppressMessage
5 | // attributes that are applied to this project.
6 | // Project-level suppressions either have no target or are given
7 | // a specific target and scoped to a namespace, type, member, etc.
8 |
9 | using System.Diagnostics.CodeAnalysis;
10 |
11 | [assembly: SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1633:File should have header", Justification = "<挂起>", Scope = "namespace", Target = "~N:Cafe.Matcha.Network.Universalis")]
12 |
--------------------------------------------------------------------------------
/Cafe.Matcha/DTO/InitZoneDTO.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) FFCafe. All rights reserved.
2 | // Licensed under the AGPL-3.0 license. See LICENSE file in the project root for full license information.
3 |
4 | namespace Cafe.Matcha.DTO
5 | {
6 | using Cafe.Matcha.Constant;
7 | using Newtonsoft.Json;
8 |
9 | internal class InitZoneDTO : BaseDTO
10 | {
11 | public override EventType EventType
12 | {
13 | get
14 | {
15 | return EventType.InitZone;
16 | }
17 | }
18 |
19 | [JsonProperty("zone")]
20 | public int Zone;
21 |
22 | [JsonProperty("instance")]
23 | public int Instance;
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/Cafe.Matcha/DTO/MatchAlertDTO.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) FFCafe. All rights reserved.
2 | // Licensed under the AGPL-3.0 license. See LICENSE file in the project root for full license information.
3 |
4 | namespace Cafe.Matcha.DTO
5 | {
6 | using Cafe.Matcha.Constant;
7 | using Newtonsoft.Json;
8 |
9 | internal class MatchAlertDTO : BaseDTO
10 | {
11 | public override EventType EventType
12 | {
13 | get
14 | {
15 | return EventType.MatchAlert;
16 | }
17 | }
18 |
19 | [JsonProperty("roulette")]
20 | public int Roulette;
21 |
22 | [JsonProperty("instance")]
23 | public int Instance;
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/Cafe.Matcha/DTO/TreasureResultDTO.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) FFCafe. All rights reserved.
2 | // Licensed under the AGPL-3.0 license. See LICENSE file in the project root for full license information.
3 |
4 | namespace Cafe.Matcha.DTO
5 | {
6 | using Cafe.Matcha.Constant;
7 | using Newtonsoft.Json;
8 |
9 | internal class TreasureResultDTO : BaseDTO
10 | {
11 | public override EventType EventType
12 | {
13 | get
14 | {
15 | return EventType.TreasureResult;
16 | }
17 | }
18 |
19 | [JsonProperty("value")]
20 | public string Value;
21 |
22 | [JsonProperty("round")]
23 | public int Round;
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/Cafe.Matcha/DTO/FateWatchListChangedDTO.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) FFCafe. All rights reserved.
2 | // Licensed under the AGPL-3.0 license. See LICENSE file in the project root for full license information.
3 |
4 | namespace Cafe.Matcha.DTO
5 | {
6 | using Cafe.Matcha.Constant;
7 | using Newtonsoft.Json;
8 |
9 | internal class FateWatchListChangedDTO : BaseDTO
10 | {
11 | public override EventType EventType
12 | {
13 | get
14 | {
15 | return EventType.FateWatchListChanged;
16 | }
17 | }
18 |
19 | [JsonProperty("world")]
20 | public uint World;
21 |
22 | [JsonProperty("fates")]
23 | public int[] Fates;
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/Cafe.Matcha/Models/WorldData.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) FFCafe. All rights reserved.
2 | // Licensed under the AGPL-3.0 license. See LICENSE file in the project root for full license information.
3 |
4 | namespace Cafe.Matcha.Models
5 | {
6 | using Newtonsoft.Json;
7 |
8 | public class WorldData
9 | {
10 | [JsonProperty("name")]
11 | public string LocalName;
12 | [JsonProperty("name_en")]
13 | public string EnglishName;
14 | [JsonProperty("dc")]
15 | public string LocalDataCenter;
16 | [JsonProperty("dc_en")]
17 | public string EnglishDataCenter;
18 |
19 | public override string ToString()
20 | {
21 | return LocalName;
22 | }
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/Cafe.Matcha/Models/Template.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) FFCafe. All rights reserved.
2 | // Licensed under the AGPL-3.0 license. See LICENSE file in the project root for full license information.
3 |
4 | namespace Cafe.Matcha.Models
5 | {
6 | using System.Collections.Generic;
7 | using Cafe.Matcha.Utils;
8 | using Newtonsoft.Json;
9 |
10 | public class Template : BindingTarget
11 | {
12 | public string LocalName
13 | {
14 | get
15 | {
16 | return Name.ToString();
17 | }
18 | }
19 |
20 | [JsonProperty("name")]
21 | public ItemName Name { get; set; }
22 |
23 | [JsonProperty("fate")]
24 | public List Fates = new List();
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/Cafe.Matcha/DTO/TreasureSpotDTO.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) FFCafe. All rights reserved.
2 | // Licensed under the AGPL-3.0 license. See LICENSE file in the project root for full license information.
3 |
4 | namespace Cafe.Matcha.DTO
5 | {
6 | using Cafe.Matcha.Constant;
7 | using Newtonsoft.Json;
8 |
9 | internal class TreasureSpotDTO : BaseDTO
10 | {
11 | public override EventType EventType
12 | {
13 | get
14 | {
15 | return EventType.TreasureSpot;
16 | }
17 | }
18 |
19 | [JsonProperty("item")]
20 | public int Item;
21 |
22 | [JsonProperty("location")]
23 | public int Location;
24 |
25 | [JsonProperty("isNew")]
26 | public bool IsNew;
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/Cafe.Matcha/Constant/EventType.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) FFCafe. All rights reserved.
2 | // Licensed under the AGPL-3.0 license. See LICENSE file in the project root for full license information.
3 |
4 | namespace Cafe.Matcha.Constant
5 | {
6 | // WARNING: The order CANNOT be changed. ONLY append to this list.
7 | public enum EventType
8 | {
9 | None = 0,
10 | Fate,
11 |
12 | MatchAlert,
13 | InitZone,
14 |
15 | FishBite,
16 | MarketBoardItemListing,
17 | MarketBoardItemListingCount,
18 |
19 | MiniCactpot,
20 | Gearset,
21 | TreasureSpot,
22 | TreasureResult,
23 | CompanyVoyageStatus,
24 |
25 | DynamicEvent,
26 | FateWatchListChanged,
27 | Queue,
28 | FishCast
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/Cafe.Matcha/DTO/FateDTO.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) FFCafe. All rights reserved.
2 | // Licensed under the AGPL-3.0 license. See LICENSE file in the project root for full license information.
3 |
4 | namespace Cafe.Matcha.DTO
5 | {
6 | using Cafe.Matcha.Constant;
7 | using Newtonsoft.Json;
8 |
9 | internal class FateDTO : BaseDTO
10 | {
11 | public override EventType EventType
12 | {
13 | get
14 | {
15 | return EventType.Fate;
16 | }
17 | }
18 |
19 | [JsonProperty("type")]
20 | public string Type;
21 |
22 | [JsonProperty("fate")]
23 | public int Fate;
24 |
25 | [JsonProperty("progress")]
26 | public int Progress = 0;
27 |
28 | [JsonProperty("extra")]
29 | public int Extra = 0;
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/Cafe.Matcha/DTO/MiniCactpotDTO.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) FFCafe. All rights reserved.
2 | // Licensed under the AGPL-3.0 license. See LICENSE file in the project root for full license information.
3 |
4 | namespace Cafe.Matcha.DTO
5 | {
6 | using Cafe.Matcha.Constant;
7 | using Newtonsoft.Json;
8 |
9 | internal class MiniCactpotDTO : BaseDTO
10 | {
11 | public override EventType EventType
12 | {
13 | get
14 | {
15 | return EventType.MiniCactpot;
16 | }
17 | }
18 |
19 | [JsonProperty("isNewGame")]
20 | public bool IsNewGame;
21 |
22 | [JsonProperty("x")]
23 | public int X;
24 |
25 | [JsonProperty("y")]
26 | public int Y;
27 |
28 | [JsonProperty("value")]
29 | public int Value;
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/Cafe.Matcha/DTO/QueueDTO.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) FFCafe. All rights reserved.
2 | // Licensed under the AGPL-3.0 license. See LICENSE file in the project root for full license information.
3 |
4 | namespace Cafe.Matcha.DTO
5 | {
6 | using Cafe.Matcha.Constant;
7 | using Newtonsoft.Json;
8 |
9 | internal class QueueDTO : BaseDTO
10 | {
11 | public override EventType EventType
12 | {
13 | get
14 | {
15 | return EventType.Queue;
16 | }
17 | }
18 |
19 | [JsonProperty("type")]
20 | public string Type;
21 |
22 | [JsonProperty("stage")]
23 | public string Stage;
24 |
25 | [JsonProperty("order")]
26 | public uint Order = 0;
27 |
28 | [JsonProperty("time")]
29 | public uint Time = 0;
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/Cafe.Matcha/Models/InstanceData.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) FFCafe. All rights reserved.
2 | // Licensed under the AGPL-3.0 license. See LICENSE file in the project root for full license information.
3 |
4 | namespace Cafe.Matcha.Models
5 | {
6 | using Newtonsoft.Json;
7 |
8 | public class InstanceData
9 | {
10 | [JsonProperty("name")]
11 | public ItemName Name;
12 |
13 | [JsonProperty("type")]
14 | public int Type;
15 |
16 | [JsonProperty("level")]
17 | public int Level;
18 |
19 | [JsonProperty("levelSync")]
20 | public int LevelSync;
21 |
22 | [JsonProperty("item")]
23 | public int ItemLevel;
24 |
25 | [JsonProperty("itemSync")]
26 | public int ItemLevelSync;
27 |
28 | [JsonProperty("memberType")]
29 | public int MemberType;
30 | }
31 | }
--------------------------------------------------------------------------------
/Cafe.Matcha/Network/Structures/IMarketBoardPurchase.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) FFCafe. All rights reserved.
2 | // Licensed under the AGPL-3.0 license. See LICENSE file in the project root for full license information.
3 |
4 | namespace Cafe.Matcha.Network.Structures
5 | {
6 | ///
7 | /// An interface that represents market board purchase information. This message is received from the
8 | /// server when a purchase is made at a market board.
9 | ///
10 | public interface IMarketBoardPurchase
11 | {
12 | ///
13 | /// Gets the item ID of the item that was purchased.
14 | ///
15 | uint CatalogId { get; }
16 |
17 | ///
18 | /// Gets the quantity of the item that was purchased.
19 | ///
20 | uint ItemQuantity { get; }
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/Cafe.Matcha/DTO/MarketBoardItemListingCountDTO.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) FFCafe. All rights reserved.
2 | // Licensed under the AGPL-3.0 license. See LICENSE file in the project root for full license information.
3 |
4 | namespace Cafe.Matcha.DTO
5 | {
6 | using System;
7 | using Cafe.Matcha.Constant;
8 | using Newtonsoft.Json;
9 |
10 | internal class MarketBoardItemListingCountDTO : BaseDTO
11 | {
12 | public override EventType EventType
13 | {
14 | get
15 | {
16 | return EventType.MarketBoardItemListingCount;
17 | }
18 | }
19 |
20 | [JsonProperty("item")]
21 | public int Item;
22 |
23 | [JsonProperty("world")]
24 | public int World = 0;
25 |
26 | [JsonProperty("count")]
27 | [Obsolete("Count is not used in overlay and difficult to obtain after 7.0")]
28 | public int Count = 0;
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/Cafe.Matcha/Network/Universalis/Types/UniversalisTaxUploadRequest.cs:
--------------------------------------------------------------------------------
1 | namespace Cafe.Matcha.Network.Universalis
2 | {
3 | using Newtonsoft.Json;
4 |
5 | ///
6 | /// A Universalis API structure.
7 | ///
8 | internal class UniversalisTaxUploadRequest
9 | {
10 | ///
11 | /// Gets or sets the uploader's ID.
12 | ///
13 | [JsonProperty("uploaderID")]
14 | public string UploaderId { get; set; }
15 |
16 | ///
17 | /// Gets or sets the world to retrieve data from.
18 | ///
19 | [JsonProperty("worldID")]
20 | public uint WorldId { get; set; }
21 |
22 | ///
23 | /// Gets or sets tax data for each city's market.
24 | ///
25 | [JsonProperty("marketTaxRates")]
26 | public UniversalisTaxData TaxData { get; set; }
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/Cafe.Matcha/Utils/FateManager.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) FFCafe. All rights reserved.
2 | // Licensed under the AGPL-3.0 license. See LICENSE file in the project root for full license information.
3 |
4 | namespace Cafe.Matcha.Utils
5 | {
6 | using System.Collections.Generic;
7 | using System.IO;
8 | using Newtonsoft.Json;
9 |
10 | internal class FateManager
11 | {
12 | public static List Load(string fileName)
13 | {
14 | try
15 | {
16 | string content = File.ReadAllText(fileName);
17 | return JsonConvert.DeserializeObject>(content);
18 | }
19 | catch
20 | {
21 | return null;
22 | }
23 | }
24 |
25 | public static void Save(string fileName, IList fates)
26 | {
27 | File.WriteAllText(fileName, JsonConvert.SerializeObject(fates, Formatting.Indented));
28 | }
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/Cafe.Matcha/data/patch.json:
--------------------------------------------------------------------------------
1 | {
2 | "2": {
3 | "chs": "重生之境",
4 | "en": "A Realm Reborn",
5 | "ja": "新生エオルゼア",
6 | "de": "A Realm Reborn",
7 | "fr": "A Realm Reborn"
8 | },
9 | "3": {
10 | "chs": "苍穹之禁城",
11 | "en": "Heavensward",
12 | "ja": "蒼天のイシュガルド",
13 | "de": "Heavensward",
14 | "fr": "Heavensward"
15 | },
16 | "4": {
17 | "chs": "红莲之狂潮",
18 | "en": "Stormblood",
19 | "ja": "紅蓮のリベレーター",
20 | "de": "Stormblood",
21 | "fr": "Stormblood"
22 | },
23 | "5": {
24 | "chs": "暗影之逆焰",
25 | "en": "Shadowbringers",
26 | "ja": "漆黒のヴィランズ",
27 | "de": "Shadowbringers",
28 | "fr": "Shadowbringers"
29 | },
30 | "6": {
31 | "chs": "晓月之终途",
32 | "en": "Endwalker",
33 | "ja": "暁月のフィナーレ",
34 | "de": "Endwalker",
35 | "fr": "Endwalker"
36 | },
37 | "7": {
38 | "chs": "金曦之遗辉",
39 | "en": "Dawntrail",
40 | "ja": "黄金のレガシー",
41 | "de": "Dawntrail",
42 | "fr": "Dawntrail"
43 | }
44 | }
--------------------------------------------------------------------------------
/Cafe.Matcha/ViewModels/TelemetrySetting.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) FFCafe. All rights reserved.
2 | // Licensed under the AGPL-3.0 license. See LICENSE file in the project root for full license information.
3 |
4 | namespace Cafe.Matcha.ViewModels
5 | {
6 | using System.Windows;
7 | using Cafe.Matcha.Utils;
8 |
9 | internal class TelemetrySetting : BindingTarget
10 | {
11 | public bool Enabled { get; set; } = true;
12 | public bool IsInit { get; set; } = false;
13 |
14 | public Visibility CheckboxVisibility
15 | {
16 | get
17 | {
18 | return IsInit ? Visibility.Hidden : Visibility.Visible;
19 | }
20 | }
21 |
22 | public string OkText
23 | {
24 | get
25 | {
26 | return IsInit ? "同意" : "确认";
27 | }
28 | }
29 |
30 | public string CancelText
31 | {
32 | get
33 | {
34 | return IsInit ? "拒绝" : "取消";
35 | }
36 | }
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/Cafe.Matcha/Views/License.xaml.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) FFCafe. All rights reserved.
2 | // Licensed under the AGPL-3.0 license. See LICENSE file in the project root for full license information.
3 |
4 | namespace Cafe.Matcha.Views
5 | {
6 | using System;
7 | using System.Diagnostics;
8 | using System.Windows;
9 | using Cafe.Matcha.Utils;
10 |
11 | public partial class License : Window
12 | {
13 | public License()
14 | {
15 | InitializeComponent();
16 | Title = "授权提示 - " + Data.Title;
17 | }
18 |
19 | private void BOK_Click(object sender, RoutedEventArgs e)
20 | {
21 | Close();
22 | }
23 |
24 | private void Hyperlink_RequestNavigate(object sender, System.Windows.Navigation.RequestNavigateEventArgs e)
25 | {
26 | Process.Start(new ProcessStartInfo(e.Uri.AbsoluteUri));
27 | e.Handled = true;
28 | }
29 |
30 | private void Window_SourceInitialized(object sender, EventArgs e)
31 | {
32 | Helper.SetDialog(this);
33 | }
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/Cafe.Matcha/DTO/DynamicEventDTO.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) FFCafe. All rights reserved.
2 | // Licensed under the AGPL-3.0 license. See LICENSE file in the project root for full license information.
3 |
4 | namespace Cafe.Matcha.DTO
5 | {
6 | using Cafe.Matcha.Constant;
7 | using Newtonsoft.Json;
8 |
9 | internal class DynamicEventDTO : BaseDTO
10 | {
11 | public override EventType EventType
12 | {
13 | get
14 | {
15 | return EventType.DynamicEvent;
16 | }
17 | }
18 |
19 | [JsonProperty("zone")]
20 | public int Zone = 0;
21 |
22 | [JsonProperty("event")]
23 | public int Event;
24 |
25 | [JsonProperty("participants")]
26 | public int Participants = 0;
27 |
28 | [JsonProperty("stage")]
29 | public int Stage = 0;
30 |
31 | [JsonProperty("progress")]
32 | public int Progress = 0;
33 |
34 | [JsonProperty("nextStage")]
35 | public uint NextStage;
36 |
37 | [JsonProperty("countdown")]
38 | public int Countdown;
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/Cafe.Matcha/Network/Universalis/Types/UniversalisItemListingsUploadRequest.cs:
--------------------------------------------------------------------------------
1 | namespace Cafe.Matcha.Network.Universalis
2 | {
3 | using System.Collections.Generic;
4 | using Newtonsoft.Json;
5 |
6 | ///
7 | /// A Universalis API structure.
8 | ///
9 | internal class UniversalisItemListingsUploadRequest
10 | {
11 | ///
12 | /// Gets or sets the world ID.
13 | ///
14 | [JsonProperty("worldID")]
15 | public uint WorldId { get; set; }
16 |
17 | ///
18 | /// Gets or sets the item ID.
19 | ///
20 | [JsonProperty("itemID")]
21 | public uint ItemId { get; set; }
22 |
23 | ///
24 | /// Gets or sets the list of available items.
25 | ///
26 | [JsonProperty("listings")]
27 | public List Listings { get; set; }
28 |
29 | ///
30 | /// Gets or sets the uploader ID.
31 | ///
32 | [JsonProperty("uploaderID")]
33 | public string UploaderId { get; set; }
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/Cafe.Matcha/DTO/MarketBoardItemListingDTO.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) FFCafe. All rights reserved.
2 | // Licensed under the AGPL-3.0 license. See LICENSE file in the project root for full license information.
3 |
4 | namespace Cafe.Matcha.DTO
5 | {
6 | using System.Collections.Generic;
7 | using Cafe.Matcha.Constant;
8 | using Newtonsoft.Json;
9 |
10 | internal class MarketBoardItemListingItem
11 | {
12 | [JsonProperty("price")]
13 | public int Price;
14 |
15 | [JsonProperty("quantity")]
16 | public int Quantity;
17 |
18 | [JsonProperty("hq")]
19 | public bool HQ;
20 | }
21 |
22 | internal class MarketBoardItemListingDTO : BaseDTO
23 | {
24 | public override EventType EventType
25 | {
26 | get
27 | {
28 | return EventType.MarketBoardItemListing;
29 | }
30 | }
31 |
32 | [JsonProperty("item")]
33 | public int Item;
34 |
35 | [JsonProperty("world")]
36 | public int World = 0;
37 |
38 | [JsonProperty("data")]
39 | public IEnumerable Data;
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/Cafe.Matcha/DTO/GearsetDTO.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) FFCafe. All rights reserved.
2 | // Licensed under the AGPL-3.0 license. See LICENSE file in the project root for full license information.
3 |
4 | namespace Cafe.Matcha.DTO
5 | {
6 | using System.Collections.Generic;
7 | using Cafe.Matcha.Constant;
8 | using Cafe.Matcha.Network.Structures;
9 | using Newtonsoft.Json;
10 |
11 | internal class GearsetDTO : BaseDTO
12 | {
13 | public override EventType EventType
14 | {
15 | get
16 | {
17 | return EventType.Gearset;
18 | }
19 | }
20 |
21 | [JsonProperty("self")]
22 | public bool IsSelf;
23 |
24 | [JsonProperty("name")]
25 | public string Name = string.Empty;
26 |
27 | [JsonProperty("slot")]
28 | public int Slot;
29 |
30 | [JsonProperty("item")]
31 | public int Item;
32 |
33 | [JsonProperty("hq")]
34 | public bool HQ;
35 |
36 | [JsonProperty("glamour")]
37 | public int Glamour;
38 |
39 | [JsonProperty("materias")]
40 | public IEnumerable Materias;
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/Cafe.Matcha/DTO/CompanyVoyageStatusDTO.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) FFCafe. All rights reserved.
2 | // Licensed under the AGPL-3.0 license. See LICENSE file in the project root for full license information.
3 |
4 | namespace Cafe.Matcha.DTO
5 | {
6 | using System.Collections.Generic;
7 | using Cafe.Matcha.Constant;
8 | using Newtonsoft.Json;
9 |
10 | internal class CompanyVoyageStatusItem
11 | {
12 | [JsonProperty("returnTime")]
13 | public uint ReturnTime;
14 |
15 | [JsonProperty("maxDistance")]
16 | public int MaxDistance;
17 |
18 | [JsonProperty("name")]
19 | public string Name;
20 |
21 | [JsonProperty("destination")]
22 | public int[] Destination;
23 | }
24 |
25 | internal class CompanyVoyageStatusDTO : BaseDTO
26 | {
27 | public override EventType EventType
28 | {
29 | get
30 | {
31 | return EventType.CompanyVoyageStatus;
32 | }
33 | }
34 |
35 | [JsonProperty("type")]
36 | public string Type;
37 |
38 | [JsonProperty("list")]
39 | public IEnumerable List;
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/Cafe.Matcha/Network/Handler/QueueHandler.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) FFCafe. All rights reserved.
2 | // Licensed under the AGPL-3.0 license. See LICENSE file in the project root for full license information.
3 |
4 | namespace Cafe.Matcha.Network.Handler
5 | {
6 | using System;
7 | using Cafe.Matcha.Constant;
8 | using Cafe.Matcha.DTO;
9 | using Cafe.Matcha.Network.Structures;
10 |
11 | internal class QueueHandler : AbstractHandler
12 | {
13 | public QueueHandler(Action fireEvent) : base(fireEvent)
14 | {
15 | }
16 |
17 | public override bool Handle(Packet packet)
18 | {
19 | if (packet.MatchaOpcode == MatchaOpcode.WorldVisitQueue)
20 | {
21 | var data = WorldVisitQueue.Read(packet.GetRawData());
22 | fireEvent(new QueueDTO()
23 | {
24 | Type = "world-visit",
25 | Stage = data.Stage == 1 ? "waiting" : data.Stage == 2 ? "ready" : data.Stage == 3 ? "done" : "unknown",
26 | Order = data.Order,
27 | Time = data.Time,
28 | });
29 |
30 | return true;
31 | }
32 |
33 | return false;
34 | }
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/Cafe.Matcha/Views/OverrideConfirm.xaml.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) FFCafe. All rights reserved.
2 | // Licensed under the AGPL-3.0 license. See LICENSE file in the project root for full license information.
3 |
4 | namespace Cafe.Matcha.Views
5 | {
6 | using System.Windows;
7 | using Cafe.Matcha.Utils;
8 |
9 | public partial class OverrideConfirm : Window
10 | {
11 | public bool All { get; set; } = false;
12 |
13 | public OverrideConfirm()
14 | {
15 | InitializeComponent();
16 | }
17 |
18 | public OverrideConfirm(string content, string title) : this()
19 | {
20 | this.Title = title;
21 | lMain.Content = content;
22 | }
23 |
24 | private void Close(bool dialogResult)
25 | {
26 | DialogResult = dialogResult;
27 | Close();
28 | }
29 |
30 | private void BYes_Click(object sender, RoutedEventArgs e)
31 | {
32 | Close(true);
33 | }
34 |
35 | private void BNo_Click(object sender, RoutedEventArgs e)
36 | {
37 | Close(false);
38 | }
39 |
40 | private void Window_SourceInitialized(object sender, System.EventArgs e)
41 | {
42 | Helper.SetDialog(this);
43 | }
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/Cafe.Matcha/Network/Structures/WorldVisitQueue.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) FFCafe. All rights reserved.
2 | // Licensed under the AGPL-3.0 license. See LICENSE file in the project root for full license information.
3 |
4 | namespace Cafe.Matcha.Network.Structures
5 | {
6 | using System.IO;
7 |
8 | public class WorldVisitQueue
9 | {
10 | public uint Stage { get; internal set; }
11 | public uint Order { get; internal set; }
12 | public uint Time { get; internal set; }
13 |
14 | ///
15 | /// Read a object from memory.
16 | ///
17 | /// Data to read.
18 | /// A new object.
19 | public static WorldVisitQueue Read(byte[] data)
20 | {
21 | using (var stream = new MemoryStream(data))
22 | {
23 | using (var reader = new BinaryReader(stream))
24 | {
25 | var output = new WorldVisitQueue();
26 | output.Stage = reader.ReadUInt32();
27 | output.Order = reader.ReadUInt32();
28 | output.Time = reader.ReadUInt32();
29 | return output;
30 | }
31 | }
32 | }
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/Cafe.Matcha/Network/Universalis/Types/UniversalisItemUploadRequest.cs:
--------------------------------------------------------------------------------
1 | namespace Cafe.Matcha.Network.Universalis
2 | {
3 | using System.Collections.Generic;
4 | using Newtonsoft.Json;
5 |
6 | ///
7 | /// A Universalis API structure.
8 | ///
9 | internal class UniversalisItemUploadRequest
10 | {
11 | ///
12 | /// Gets or sets the world ID.
13 | ///
14 | [JsonProperty("worldID")]
15 | public uint WorldId { get; set; }
16 |
17 | ///
18 | /// Gets or sets the item ID.
19 | ///
20 | [JsonProperty("itemID")]
21 | public uint ItemId { get; set; }
22 |
23 | ///
24 | /// Gets or sets the list of available items.
25 | ///
26 | [JsonProperty("listings")]
27 | public List Listings { get; set; }
28 |
29 | ///
30 | /// Gets or sets the list of available entries.
31 | ///
32 | [JsonProperty("entries")]
33 | public List Sales { get; set; }
34 |
35 | ///
36 | /// Gets or sets the uploader ID.
37 | ///
38 | [JsonProperty("uploaderID")]
39 | public string UploaderId { get; set; }
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/Cafe.Matcha/Models/ItemName.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) FFCafe. All rights reserved.
2 | // Licensed under the AGPL-3.0 license. See LICENSE file in the project root for full license information.
3 |
4 | namespace Cafe.Matcha.Models
5 | {
6 | using Newtonsoft.Json;
7 |
8 | public class ItemName
9 | {
10 | [JsonProperty("chs")]
11 | public string Chinese = null;
12 | [JsonProperty("en")]
13 | public string English = null;
14 | [JsonProperty("ja")]
15 | public string Japanese = null;
16 | [JsonProperty("de")]
17 | public string German = null;
18 | [JsonProperty("fr")]
19 | public string French = null;
20 |
21 | public override string ToString()
22 | {
23 | switch (Config.Instance.Language)
24 | {
25 | case FFXIV_ACT_Plugin.Common.Language.French:
26 | return French ?? English;
27 | case FFXIV_ACT_Plugin.Common.Language.German:
28 | return German ?? English;
29 | case FFXIV_ACT_Plugin.Common.Language.Japanese:
30 | return Japanese ?? English;
31 | case FFXIV_ACT_Plugin.Common.Language.Chinese:
32 | return Chinese ?? English;
33 | default:
34 | return English;
35 | }
36 | }
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/Cafe.Matcha/Network/Universalis/Types/UniversalisItemListingDeleteRequest.cs:
--------------------------------------------------------------------------------
1 | namespace Cafe.Matcha.Network.Universalis
2 | {
3 | using Newtonsoft.Json;
4 |
5 | ///
6 | /// Request payload for market board purchases.
7 | ///
8 | internal class UniversalisItemListingDeleteRequest
9 | {
10 | ///
11 | /// Gets or sets the object ID of the retainer associated with the sale.
12 | ///
13 | [JsonProperty("retainerID")]
14 | public string RetainerId { get; set; }
15 |
16 | ///
17 | /// Gets or sets the object ID of the item listing.
18 | ///
19 | [JsonProperty("listingID")]
20 | public string ListingId { get; set; }
21 |
22 | ///
23 | /// Gets or sets the quantity of the item that was purchased.
24 | ///
25 | [JsonProperty("quantity")]
26 | public uint Quantity { get; set; }
27 |
28 | ///
29 | /// Gets or sets the unit price of the item.
30 | ///
31 | [JsonProperty("pricePerUnit")]
32 | public uint PricePerUnit { get; set; }
33 |
34 | ///
35 | /// Gets or sets the uploader ID.
36 | ///
37 | [JsonProperty("uploaderID")]
38 | public string UploaderId { get; set; }
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/Cafe.Matcha/Utils/BindingTarget.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) FFCafe. All rights reserved.
2 | // Licensed under the AGPL-3.0 license. See LICENSE file in the project root for full license information.
3 |
4 | namespace Cafe.Matcha.Utils
5 | {
6 | using System.Collections.Generic;
7 | using System.Collections.ObjectModel;
8 | using System.Collections.Specialized;
9 | using System.ComponentModel;
10 |
11 | public class BindingTarget : INotifyPropertyChanged
12 | {
13 | protected void EmitPropertyChanged(string propertyName)
14 | {
15 | PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
16 | }
17 |
18 | public event PropertyChangedEventHandler PropertyChanged;
19 | }
20 |
21 | public class StaticBindingTarget : BindingTarget where T : new()
22 | {
23 | public static T Instance { get; } = new T();
24 | }
25 |
26 | public class ListBindingTarget : ObservableCollection
27 | {
28 | public ListBindingTarget() : base() { }
29 | public ListBindingTarget(List list) : base(list) { }
30 | public ListBindingTarget(IEnumerable collection) : base(collection) { }
31 |
32 | public void EmitCollectionChanged(NotifyCollectionChangedEventArgs e)
33 | {
34 | OnCollectionChanged(e);
35 | }
36 |
37 | public void EmitPropertyChanged(PropertyChangedEventArgs e)
38 | {
39 | OnPropertyChanged(e);
40 | }
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/Cafe.Matcha/Views/OverrideConfirm.xaml:
--------------------------------------------------------------------------------
1 |
8 |
9 |
10 |
11 |
12 | 不再询问 (_A)
13 |
14 |
17 |
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/Cafe.Matcha/Network/Universalis/Types/UniversalisTaxData.cs:
--------------------------------------------------------------------------------
1 | namespace Cafe.Matcha.Network.Universalis
2 | {
3 | using Newtonsoft.Json;
4 |
5 | ///
6 | /// A Universalis API structure.
7 | ///
8 | internal class UniversalisTaxData
9 | {
10 | ///
11 | /// Gets or sets Limsa Lominsa's current tax rate.
12 | ///
13 | [JsonProperty("limsaLominsa")]
14 | public uint LimsaLominsa { get; set; }
15 |
16 | ///
17 | /// Gets or sets Gridania's current tax rate.
18 | ///
19 | [JsonProperty("gridania")]
20 | public uint Gridania { get; set; }
21 |
22 | ///
23 | /// Gets or sets Ul'dah's current tax rate.
24 | ///
25 | [JsonProperty("uldah")]
26 | public uint Uldah { get; set; }
27 |
28 | ///
29 | /// Gets or sets Ishgard's current tax rate.
30 | ///
31 | [JsonProperty("ishgard")]
32 | public uint Ishgard { get; set; }
33 |
34 | ///
35 | /// Gets or sets Kugane's current tax rate.
36 | ///
37 | [JsonProperty("kugane")]
38 | public uint Kugane { get; set; }
39 |
40 | ///
41 | /// Gets or sets The Crystarium's current tax rate.
42 | ///
43 | [JsonProperty("crystarium")]
44 | public uint Crystarium { get; set; }
45 |
46 | ///
47 | /// Gets or sets Old Sharlayan's current tax rate.
48 | ///
49 | [JsonProperty("sharlayan")]
50 | public uint Sharlayan { get; set; }
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/Cafe.Matcha/Network/Structures/IMarketBoardPurchaseHandler.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) FFCafe. All rights reserved.
2 | // Licensed under the AGPL-3.0 license. See LICENSE file in the project root for full license information.
3 |
4 | namespace Cafe.Matcha.Network.Structures
5 | {
6 | ///
7 | /// An interface that represents market board purchase information. This message is sent from the
8 | /// client when a purchase is made at a market board.
9 | ///
10 | public interface IMarketBoardPurchaseHandler
11 | {
12 | ///
13 | /// Gets the object ID of the retainer associated with the sale.
14 | ///
15 | ulong RetainerId { get; }
16 |
17 | ///
18 | /// Gets the object ID of the item listing.
19 | ///
20 | ulong ListingId { get; }
21 |
22 | ///
23 | /// Gets the item ID of the item that was purchased.
24 | ///
25 | uint CatalogId { get; }
26 |
27 | ///
28 | /// Gets the quantity of the item that was purchased.
29 | ///
30 | uint ItemQuantity { get; }
31 |
32 | ///
33 | /// Gets the unit price of the item.
34 | ///
35 | uint PricePerUnit { get; }
36 |
37 | ///
38 | /// Gets a value indicating whether the item is HQ.
39 | ///
40 | bool IsHq { get; }
41 |
42 | ///
43 | /// Gets the total tax.
44 | ///
45 | uint TotalTax { get; }
46 |
47 | ///
48 | /// Gets the city ID of the retainer selling the item.
49 | ///
50 | int RetainerCityId { get; }
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/Cafe.Matcha/Network/Structures/MarketBoardPurchase.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) FFCafe. All rights reserved.
2 | // Licensed under the AGPL-3.0 license. See LICENSE file in the project root for full license information.
3 |
4 | namespace Cafe.Matcha.Network.Structures
5 | {
6 | using System.IO;
7 |
8 | ///
9 | /// Represents market board purchase information. This message is received from the
10 | /// server when a purchase is made at a market board.
11 | ///
12 | public class MarketBoardPurchase : IMarketBoardPurchase
13 | {
14 | private MarketBoardPurchase()
15 | {
16 | }
17 |
18 | ///
19 | /// Gets the item ID of the item that was purchased.
20 | ///
21 | public uint CatalogId { get; private set; }
22 |
23 | ///
24 | /// Gets the quantity of the item that was purchased.
25 | ///
26 | public uint ItemQuantity { get; private set; }
27 |
28 | ///
29 | /// Reads market board purchase information from the struct at the provided pointer.
30 | ///
31 | /// Data to read.
32 | /// An object representing the data read.
33 | public static MarketBoardPurchase Read(byte[] data)
34 | {
35 | using (var stream = new MemoryStream(data))
36 | {
37 | using (var reader = new BinaryReader(stream))
38 | {
39 | var output = new MarketBoardPurchase();
40 |
41 | output.CatalogId = reader.ReadUInt32();
42 | reader.ReadBytes(0x4); // Padding
43 | output.ItemQuantity = reader.ReadUInt32();
44 |
45 | return output;
46 | }
47 | }
48 | }
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/Cafe.Matcha/Network/Universalis/Types/UniversalisHistoryEntry.cs:
--------------------------------------------------------------------------------
1 | namespace Cafe.Matcha.Network.Universalis
2 | {
3 | using Newtonsoft.Json;
4 |
5 | ///
6 | /// A Universalis API structure.
7 | ///
8 | internal class UniversalisHistoryEntry
9 | {
10 | ///
11 | /// Gets or sets a value indicating whether the item is HQ or not.
12 | ///
13 | [JsonProperty("hq")]
14 | public bool Hq { get; set; }
15 |
16 | ///
17 | /// Gets or sets the item price per unit.
18 | ///
19 | [JsonProperty("pricePerUnit")]
20 | public uint PricePerUnit { get; set; }
21 |
22 | ///
23 | /// Gets or sets the quantity of items available.
24 | ///
25 | [JsonProperty("quantity")]
26 | public uint Quantity { get; set; }
27 |
28 | ///
29 | /// Gets or sets the name of the buyer.
30 | ///
31 | [JsonProperty("buyerName")]
32 | public string BuyerName { get; set; }
33 |
34 | ///
35 | /// Gets or sets a value indicating whether this item was on a mannequin.
36 | ///
37 | [JsonProperty("onMannequin")]
38 | public bool OnMannequin { get; set; }
39 |
40 | ///
41 | /// Gets or sets the seller ID.
42 | ///
43 | [JsonProperty("sellerID")]
44 | public string SellerId { get; set; }
45 |
46 | ///
47 | /// Gets or sets the buyer ID.
48 | ///
49 | [JsonProperty("buyerID")]
50 | public string BuyerId { get; set; }
51 |
52 | ///
53 | /// Gets or sets the timestamp of the transaction.
54 | ///
55 | [JsonProperty("timestamp")]
56 | public long Timestamp { get; set; }
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/Cafe.Matcha/Network/Structures/IMarketBoardHistory.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) FFCafe. All rights reserved.
2 | // Licensed under the AGPL-3.0 license. See LICENSE file in the project root for full license information.
3 |
4 | namespace Cafe.Matcha.Network.Structures
5 | {
6 | using System;
7 | using System.Collections.Generic;
8 |
9 | ///
10 | /// An interface that represents the market board history from the game.
11 | ///
12 | public interface IMarketBoardHistory
13 | {
14 | ///
15 | /// Gets the item ID.
16 | ///
17 | uint ItemId { get; }
18 |
19 | ///
20 | /// Gets the list of individual item history listings.
21 | ///
22 | IReadOnlyList HistoryListings { get; }
23 | }
24 |
25 | ///
26 | /// An interface that represents the market board history of a single item from .
27 | ///
28 | public interface IMarketBoardHistoryListing
29 | {
30 | ///
31 | /// Gets the buyer's name.
32 | ///
33 | string BuyerName { get; }
34 |
35 | ///
36 | /// Gets a value indicating whether the item is HQ.
37 | ///
38 | bool IsHq { get; }
39 |
40 | ///
41 | /// Gets a value indicating whether the item is on a mannequin.
42 | ///
43 | bool OnMannequin { get; }
44 |
45 | ///
46 | /// Gets the time of purchase.
47 | ///
48 | DateTime PurchaseTime { get; }
49 |
50 | ///
51 | /// Gets the quantity.
52 | ///
53 | uint Quantity { get; }
54 |
55 | ///
56 | /// Gets the sale price.
57 | ///
58 | uint SalePrice { get; }
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/Cafe.Matcha/Network/Structures/IMarketTaxRates.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) FFCafe. All rights reserved.
2 | // Licensed under the AGPL-3.0 license. See LICENSE file in the project root for full license information.
3 |
4 | namespace Cafe.Matcha.Network.Structures
5 | {
6 | using System;
7 |
8 | ///
9 | /// An interface that represents the tax rates received by the client when interacting with a retainer vocate.
10 | ///
11 | public interface IMarketTaxRates
12 | {
13 | ///
14 | /// Gets the category of this ResultDialog packet.
15 | ///
16 | uint Category { get; }
17 |
18 | ///
19 | /// Gets the tax rate in Limsa Lominsa.
20 | ///
21 | uint LimsaLominsaTax { get; }
22 |
23 | ///
24 | /// Gets the tax rate in Gridania.
25 | ///
26 | uint GridaniaTax { get; }
27 |
28 | ///
29 | /// Gets the tax rate in Ul'dah.
30 | ///
31 | uint UldahTax { get; }
32 |
33 | ///
34 | /// Gets the tax rate in Ishgard.
35 | ///
36 | uint IshgardTax { get; }
37 |
38 | ///
39 | /// Gets the tax rate in Kugane.
40 | ///
41 | uint KuganeTax { get; }
42 |
43 | ///
44 | /// Gets the tax rate in the Crystarium.
45 | ///
46 | uint CrystariumTax { get; }
47 |
48 | ///
49 | /// Gets the tax rate in the Crystarium.
50 | ///
51 | uint SharlayanTax { get; }
52 |
53 | ///
54 | /// Gets the tax rate in Tuliyollal.
55 | ///
56 | uint TuliyollalTax { get; }
57 |
58 | ///
59 | /// Gets until when these values are valid.
60 | ///
61 | DateTime ValidUntil { get; }
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/Cafe.Matcha/Views/License.xaml:
--------------------------------------------------------------------------------
1 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 | 本工具当前版本基于 AGPL v3.0 发布,您可以在以下地址取得源码和二进制文件:
18 |
19 |
20 | https://github.com/thewakingsands/matcha
21 | (或主界面右上方的 GitHub 图标)
22 |
23 |
24 |
25 | 本工具免费发布,不对功能提供任何担保,且未通过任何渠道出售。如果您在获取本工具时支付了任何费用,您可能已经上当受骗。
26 |
27 |
28 |
29 |
30 |
32 |
33 |
34 |
35 |
--------------------------------------------------------------------------------
/Cafe.Matcha/Cafe.Matcha.crproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
32 |
33 |
34 | C:\Program Files (x86)\Windows Kits\10\References\10.0.18362.0\Windows.Foundation.UniversalApiContract\8.0.0.0
35 | thirdparty\FFXIV_ACT_Plugin
36 | thirdparty\ACT
37 | ..\..\..\..\.nuget\packages\recursivechangenotifier\0.4.0-b4\lib\netstandard1.0
38 | ..\..\..\..\.nuget\packages\newtonsoft.json\12.0.2\lib\net45
39 |
--------------------------------------------------------------------------------
/Cafe.Matcha/Config.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) FFCafe. All rights reserved.
2 | // Licensed under the AGPL-3.0 license. See LICENSE file in the project root for full license information.
3 |
4 | namespace Cafe.Matcha
5 | {
6 | using System.IO;
7 | using Cafe.Matcha.Utils;
8 | using Newtonsoft.Json;
9 | using ThomasJaworski.ComponentModel;
10 |
11 | public sealed class Config : BindingTarget
12 | {
13 | private static string configPath = Path.Combine(Helper.GetConfigDir(), "Cafe.Matcha.config");
14 | public static Models.ConfigData Instance { get; private set; } = new Models.ConfigData();
15 | public static void Load()
16 | {
17 | try
18 | {
19 | string content = File.ReadAllText(configPath);
20 | Instance = JsonConvert.DeserializeObject(content);
21 | }
22 | catch { }
23 |
24 | var listener = ChangeListener.Create(Instance);
25 | listener.PropertyChanged += (_, e) => Save();
26 | listener.CollectionChanged += (_, e) => Save();
27 | }
28 |
29 | public static void Save()
30 | {
31 | File.WriteAllText(configPath, JsonConvert.SerializeObject(Instance, Formatting.Indented));
32 | }
33 |
34 | public static string GetLanguageString()
35 | {
36 | switch (Instance.Language)
37 | {
38 | case FFXIV_ACT_Plugin.Common.Language.Chinese:
39 | return "chs";
40 | case FFXIV_ACT_Plugin.Common.Language.English:
41 | return "en";
42 | case FFXIV_ACT_Plugin.Common.Language.French:
43 | return "fr";
44 | case FFXIV_ACT_Plugin.Common.Language.German:
45 | return "de";
46 | case FFXIV_ACT_Plugin.Common.Language.Japanese:
47 | return "ja";
48 | default:
49 | return "en";
50 | }
51 | }
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/Cafe.Matcha/Utils/Log.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) FFCafe. All rights reserved.
2 | // Licensed under the AGPL-3.0 license. See LICENSE file in the project root for full license information.
3 |
4 | namespace Cafe.Matcha.Utils
5 | {
6 | using System.Text;
7 | using Cafe.Matcha.Constant;
8 |
9 | internal class Log
10 | {
11 | public delegate void EventHandler(LogType type, char level, string message);
12 | public static event EventHandler Handler;
13 | public static void Add(LogType type, char level, string message)
14 | {
15 | Handler?.Invoke(type, level, message);
16 | }
17 |
18 | public static void Error(LogType type, string message)
19 | {
20 | Add(type, 'E', message);
21 | }
22 |
23 | public static void Warn(LogType type, string message)
24 | {
25 | Add(type, 'W', message);
26 | }
27 |
28 | public static void Info(LogType type, string message)
29 | {
30 | Add(type, 'I', message);
31 | }
32 |
33 | #if DEBUG
34 | public static void Debug(LogType type, string message)
35 | {
36 | Add(type, 'D', message);
37 | }
38 |
39 | public static void Packet(byte[] byteArray)
40 | {
41 | StringBuilder hexDump = new StringBuilder();
42 | const int lineLength = 16;
43 |
44 | for (int i = 0; i < byteArray.Length; i += lineLength)
45 | {
46 | hexDump.AppendFormat("{0:X8}: ", i);
47 | for (int j = 0; j < lineLength; j++)
48 | {
49 | if (i + j >= byteArray.Length)
50 | {
51 | break;
52 | }
53 |
54 | byte b = byteArray[i + j];
55 | hexDump.Append(b.ToString("X2"));
56 | hexDump.Append(" ");
57 | }
58 |
59 | hexDump.AppendLine();
60 | }
61 |
62 | Add(LogType.RawPacket, 'D', hexDump.ToString());
63 | }
64 | #endif
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/Cafe.Matcha/Network/Handler/FishingHandler.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) FFCafe. All rights reserved.
2 | // Licensed under the AGPL-3.0 license. See LICENSE file in the project root for full license information.
3 |
4 | namespace Cafe.Matcha.Network.Handler
5 | {
6 | using System;
7 | using Cafe.Matcha.Constant;
8 | using Cafe.Matcha.DTO;
9 | using Cafe.Matcha.Utils;
10 |
11 | internal class FishingHandler : AbstractHandler
12 | {
13 | public FishingHandler(Action fireEvent) : base(fireEvent)
14 | {
15 | }
16 |
17 | public override bool Handle(Packet packet)
18 | {
19 | // Bite
20 | if (packet.MatchaOpcode == MatchaOpcode.EventPlay)
21 | {
22 | if (packet.Length != 72)
23 | {
24 | return false;
25 | }
26 |
27 | var targetActorId = packet.Target;
28 | var fishActorId = packet.ReadUInt32(0);
29 |
30 | if (targetActorId != fishActorId)
31 | {
32 | return true;
33 | }
34 |
35 | var type = (FishEventType)packet.ReadUInt16(12);
36 | var biteType = packet.ReadUInt16(28);
37 |
38 | if (type != FishEventType.Bite)
39 | {
40 | return true;
41 | }
42 |
43 | var biteTypeParsed = ((FishEventBiteType)biteType) switch
44 | {
45 | FishEventBiteType.Light => 1,
46 | FishEventBiteType.Medium => 2,
47 | FishEventBiteType.Big => 3,
48 | _ => 0,
49 | };
50 |
51 | if (biteTypeParsed != 0)
52 | {
53 | fireEvent(new FishBiteDTO()
54 | {
55 | Time = Helper.Now,
56 | Type = biteTypeParsed
57 | });
58 | }
59 |
60 | return true;
61 | }
62 |
63 | return false;
64 | }
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/Cafe.Matcha/Views/TelemetrySetting.xaml.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) FFCafe. All rights reserved.
2 | // Licensed under the AGPL-3.0 license. See LICENSE file in the project root for full license information.
3 |
4 | namespace Cafe.Matcha.Views
5 | {
6 | using System;
7 | using System.Diagnostics;
8 | using System.Windows;
9 | using Cafe.Matcha.Utils;
10 |
11 | public partial class TelemetrySetting : Window
12 | {
13 | private ViewModels.TelemetrySetting _viewModel = null;
14 | private ViewModels.TelemetrySetting ViewModel
15 | {
16 | get
17 | {
18 | if (_viewModel == null)
19 | {
20 | _viewModel = (ViewModels.TelemetrySetting)DataContext;
21 | }
22 |
23 | return _viewModel;
24 | }
25 | }
26 |
27 | public TelemetrySetting(bool shouldUpdate = false)
28 | {
29 | InitializeComponent();
30 | Title = "公共数据汇报设置 - " + Data.Title;
31 |
32 | ViewModel.IsInit = shouldUpdate;
33 | if (!ViewModel.IsInit)
34 | {
35 | ViewModel.Enabled = Telemetry.Instance.Enabled;
36 | }
37 | else
38 | {
39 | ViewModel.Enabled = true;
40 | }
41 | }
42 |
43 | private void BOK_Click(object sender, RoutedEventArgs e)
44 | {
45 | Telemetry.Instance.Enabled = ViewModel.Enabled;
46 | Close();
47 | }
48 |
49 | private void BCancel_Click(object sender, RoutedEventArgs e)
50 | {
51 | if (ViewModel.IsInit)
52 | {
53 | Telemetry.Instance.Enabled = false;
54 | }
55 |
56 | Close();
57 | }
58 |
59 | private void Hyperlink_RequestNavigate(object sender, System.Windows.Navigation.RequestNavigateEventArgs e)
60 | {
61 | Process.Start(new ProcessStartInfo(e.Uri.AbsoluteUri));
62 | e.Handled = true;
63 | }
64 |
65 | private void Window_SourceInitialized(object sender, EventArgs e)
66 | {
67 | Helper.SetDialog(this);
68 | }
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/Cafe.Matcha.Packer/Program.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) FFCafe. All rights reserved.
2 | // Licensed under the AGPL-3.0 license. See LICENSE file in the project root for full license information.
3 |
4 | namespace Cafe.Matcha.Packer
5 | {
6 | using System;
7 | using System.Diagnostics;
8 | using System.IO;
9 | using System.IO.Compression;
10 |
11 | public class Program
12 | {
13 | private const string Manifest = @"{
14 | ""entryPoint"": ""Plugins/Cafe.Matcha/Cafe.Matcha.dll""
15 | }
16 | ";
17 | private static void Bundle(string env)
18 | {
19 | var root = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, env);
20 | var entry = Path.Combine(root, "Cafe.Matcha.dll");
21 | var version = FileVersionInfo.GetVersionInfo(entry);
22 |
23 | var outName = $"Cafe.Matcha-{version.FileVersion}-{env}.zip";
24 |
25 | using var ms = new MemoryStream();
26 | using (var archive = new ZipArchive(ms, ZipArchiveMode.Create, true))
27 | {
28 | var manifestEntry = archive.CreateEntry("manifest.json");
29 | using (var entryStream = manifestEntry.Open())
30 | using (var streamWriter = new StreamWriter(entryStream))
31 | {
32 | streamWriter.Write(Manifest);
33 | }
34 |
35 | foreach (var dll in Directory.GetFiles(root, "*.dll"))
36 | {
37 | archive.CreateEntryFromFile(dll, @"Plugins/Cafe.Matcha/" + Path.GetFileName(dll));
38 | }
39 |
40 | foreach (var data in Directory.GetFiles(Path.Combine(root, "data"), "*.json"))
41 | {
42 | archive.CreateEntryFromFile(data, @"Plugins/Cafe.Matcha/data/" + Path.GetFileName(data));
43 | }
44 | }
45 |
46 | using var fileStream = new FileStream(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, outName), FileMode.Create);
47 | ms.Seek(0, SeekOrigin.Begin);
48 | ms.CopyTo(fileStream);
49 | }
50 |
51 | private static void Main(string[] args)
52 | {
53 | string env = args.Length >= 1 ? args[0] : "Release";
54 | Bundle(env);
55 | }
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/Cafe.Matcha/Network/Structures/MarketBoardItemRequest.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) FFCafe. All rights reserved.
2 | // Licensed under the AGPL-3.0 license. See LICENSE file in the project root for full license information.
3 |
4 | namespace Cafe.Matcha.Network.Structures
5 | {
6 | using System.Collections.Generic;
7 | using System.IO;
8 |
9 | internal class MarketBoardItemRequest
10 | {
11 | ///
12 | /// Gets the request status. Nonzero statuses are errors.
13 | /// Known values: default=0; rate limited=0x70000003.
14 | ///
15 | public uint Status { get; private set; }
16 |
17 | ///
18 | /// Gets a value indicating whether or not this request was successful.
19 | ///
20 | public bool Ok => this.Status == 0;
21 |
22 | ///
23 | /// Gets the amount to arrive.
24 | ///
25 | public uint AmountToArrive { get; private set; }
26 |
27 | ///
28 | /// Gets the offered item listings.
29 | ///
30 | public List Listings { get; } = new List();
31 |
32 | ///
33 | /// Gets the historical item listings.
34 | ///
35 | public List History { get; } = new List();
36 |
37 | ///
38 | /// Read a packet off the wire.
39 | ///
40 | /// Packet data (without header).
41 | /// An object representing the data read.
42 | public static MarketBoardItemRequest Read(byte[] data)
43 | {
44 | using (var stream = new MemoryStream(data))
45 | {
46 | using (var reader = new BinaryReader(stream))
47 | {
48 | var output = new MarketBoardItemRequest
49 | {
50 | Status = reader.ReadUInt32(),
51 | AmountToArrive = reader.ReadUInt32(),
52 | };
53 |
54 | return output;
55 | }
56 | }
57 | }
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/Cafe.Matcha/MatchaInit.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) FFCafe. All rights reserved.
2 | // Licensed under the AGPL-3.0 license. See LICENSE file in the project root for full license information.
3 |
4 | namespace Cafe.Matcha
5 | {
6 | #if DEBUG
7 | using System.Reflection;
8 | #endif
9 | using System.Windows.Forms;
10 | using System.Windows.Forms.Integration;
11 | using Advanced_Combat_Tracker;
12 | using Cafe.Matcha.Utils;
13 |
14 | #if DEBUG
15 | public static class ReflectionExtensions
16 | {
17 | public static T GetFieldValue(this object obj, string name)
18 | {
19 | // Set the flags so that private and public fields from instances will be found
20 | var bindingFlags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance;
21 | var field = obj.GetType().GetField(name, bindingFlags);
22 | return (T)field?.GetValue(obj);
23 | }
24 | }
25 | #endif
26 |
27 | public class MatchaInit : IActPluginV1
28 | {
29 | private Label lblStatus;
30 | private Views.MainControl mainControl = null;
31 |
32 | public void InitPlugin(TabPage pluginScreenSpace, Label pluginStatusText)
33 | {
34 | #if DEBUG
35 | var globalTabControl = ActGlobals.oFormActMain.GetFieldValue("tc1");
36 | var pluginsTabPage = ActGlobals.oFormActMain.GetFieldValue("tpPlugins");
37 | var pluginsTabControl = ActGlobals.oFormActMain.GetFieldValue("tcPlugins");
38 |
39 | globalTabControl.SelectedTab = pluginsTabPage;
40 | pluginsTabControl.SelectedTab = pluginScreenSpace;
41 | #endif
42 |
43 | Helper.Instance = this;
44 |
45 | lblStatus = pluginStatusText;
46 | lblStatus.Text = "Cafe.Matcha Started.";
47 | pluginScreenSpace.Text = Data.Title;
48 |
49 | if (mainControl == null)
50 | {
51 | mainControl = new Views.MainControl();
52 | var host = new ElementHost()
53 | {
54 | Dock = DockStyle.Fill,
55 | Child = mainControl
56 | };
57 |
58 | pluginScreenSpace.Controls.Add(host);
59 | }
60 | }
61 |
62 | public void DeInitPlugin()
63 | {
64 | if (lblStatus != null)
65 | {
66 | lblStatus.Text = "Cafe.Matcha Unloaded.";
67 | lblStatus = null;
68 | }
69 |
70 | if (mainControl != null)
71 | {
72 | mainControl.DeInit();
73 | }
74 | }
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/Cafe.Matcha/Network/Universalis/Client.cs:
--------------------------------------------------------------------------------
1 | namespace Cafe.Matcha.Network.Universalis
2 | {
3 | using System;
4 | using System.Linq;
5 | using Cafe.Matcha.Constant;
6 | using Cafe.Matcha.DTO;
7 | using Cafe.Matcha.Network.Handler;
8 | using Cafe.Matcha.Utils;
9 |
10 | internal class Client : AbstractHandler
11 | {
12 | public PacketProcessor UniversalisProcessor = new PacketProcessor(Secret.UniversalisKey);
13 |
14 | private bool Enabled => Config.Instance.Overlay.Universalis;
15 | private object objLock = new object();
16 |
17 | public Client(Action fireEvent) : base(fireEvent)
18 | {
19 | }
20 |
21 | public override bool Handle(Packet packet)
22 | {
23 | var opcode = packet.MatchaOpcode;
24 | if (opcode == MatchaOpcode.PlayerSetup
25 | || opcode == MatchaOpcode.MarketBoardItemListingCount
26 | || opcode == MatchaOpcode.MarketBoardItemListing
27 | || opcode == MatchaOpcode.MarketBoardItemListingHistory)
28 | {
29 | lock (objLock)
30 | {
31 | UniversalisProcessor.ProcessZonePacket(opcode, packet);
32 | }
33 | }
34 |
35 | return false;
36 | }
37 |
38 | public async void QueryItem(ushort worldId, uint itemId)
39 | {
40 | if (!Enabled || ParsePlugin.Instance == null)
41 | {
42 | return;
43 | }
44 |
45 | var items = await Api.ListByDC(worldId, itemId);
46 | if (items == null)
47 | {
48 | return;
49 | }
50 |
51 | foreach (var pair in items)
52 | {
53 | var server = pair.Key;
54 | if (server == worldId)
55 | {
56 | continue;
57 | }
58 |
59 | fireEvent(new MarketBoardItemListingCountDTO()
60 | {
61 | Item = (int)itemId,
62 | World = server,
63 | Count = pair.Value.Count
64 | });
65 |
66 | fireEvent(new MarketBoardItemListingDTO()
67 | {
68 | Item = (int)itemId,
69 | World = server,
70 | Data = pair.Value.Select((row) => new MarketBoardItemListingItem()
71 | {
72 | Price = row.PricePerUint,
73 | Quantity = row.Quantity,
74 | HQ = row.Hq
75 | })
76 | });
77 | }
78 | }
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/Cafe.Matcha/Models/TelemetryData.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) FFCafe. All rights reserved.
2 | // Licensed under the AGPL-3.0 license. See LICENSE file in the project root for full license information.
3 |
4 | namespace Cafe.Matcha.Models
5 | {
6 | using System;
7 | using Newtonsoft.Json;
8 |
9 | internal abstract class TelemetryData
10 | {
11 | public TelemetryData()
12 | {
13 | ClientId = Config.Instance.Telemetry.UUID;
14 | Version = Data.Version;
15 | Date = DateTime.Now.ToString("yyyy-MM-dd");
16 | Timestamp = DateTimeOffset.Now.ToUnixTimeMilliseconds();
17 |
18 | World = Network.State.Instance.WorldId;
19 | Server = Network.State.Instance.ServerId;
20 | Zone = Network.State.Instance.ZoneId;
21 | Instance = Network.State.Instance.InstanceId;
22 | }
23 |
24 | [JsonProperty("world")]
25 | public ushort World = 0;
26 |
27 | [JsonProperty("server")]
28 | public ushort Server = 0;
29 |
30 | [JsonProperty("zone")]
31 | public ushort Zone = 0;
32 |
33 | [JsonProperty("instance")]
34 | public ushort Instance = 0;
35 |
36 | [JsonProperty("client_id")]
37 | public string ClientId;
38 |
39 | [JsonProperty("version")]
40 | public string Version;
41 |
42 | [JsonProperty("date")]
43 | public string Date;
44 |
45 | [JsonProperty("timestamp")]
46 | public long Timestamp;
47 |
48 | public bool Equals(TelemetryData fateInit)
49 | {
50 | return Server == fateInit.Server && Zone == fateInit.Zone;
51 | }
52 |
53 | public override bool Equals(object obj)
54 | {
55 | if (ReferenceEquals(null, obj))
56 | {
57 | return false;
58 | }
59 |
60 | if (ReferenceEquals(this, obj))
61 | {
62 | return true;
63 | }
64 |
65 | return obj.GetType() == GetType() && Equals((TelemetryData)obj);
66 | }
67 |
68 | public override int GetHashCode()
69 | {
70 | return Server.GetHashCode() ^ Zone.GetHashCode() ^ Timestamp.GetHashCode();
71 | }
72 |
73 | public override string ToString()
74 | {
75 | return $"TData {{ World={World}, Server={Server}, Zone={Zone}, Instance={Instance} }} {base.ToString()}";
76 | }
77 |
78 | protected void Merge(TelemetryData data)
79 | {
80 | World = data.World;
81 | Server = data.Server;
82 | Zone = data.Zone;
83 | Instance = data.Instance;
84 | }
85 |
86 | public abstract bool TryMerge(TelemetryData data);
87 | }
88 | }
89 |
--------------------------------------------------------------------------------
/Cafe.Matcha/Views/TelemetrySetting.xaml:
--------------------------------------------------------------------------------
1 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 | 启用公共数据汇报可以帮助我们提供更准确的数据,并修复插件问题、适配新版本。
22 |
23 |
24 |
25 | 有关公共数据汇报的具体细节,请查看
26 | 隐私声明
27 | 。
28 |
29 |
30 | 由于您是首次启用本插件或隐私声明已更新,请您确认是否启用公共数据汇报。
31 |
32 |
33 | 注意:依照《通用数据保护条例》,欧盟用户请勿启用公共数据汇报
34 |
35 |
36 | Notice: Accroding to GDPR, EU users should not enable telemetry.
37 |
38 |
39 |
40 |
42 | 启用公共数据汇报
43 |
44 |
46 |
48 |
49 |
50 |
51 |
--------------------------------------------------------------------------------
/Cafe.Matcha.sln:
--------------------------------------------------------------------------------
1 |
2 | Microsoft Visual Studio Solution File, Format Version 12.00
3 | # Visual Studio Version 16
4 | VisualStudioVersion = 16.0.29209.62
5 | MinimumVisualStudioVersion = 10.0.40219.1
6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Cafe.Matcha", "Cafe.Matcha\Cafe.Matcha.csproj", "{0A8BC2CF-BCBA-4A7F-974A-11F078BED997}"
7 | ProjectSection(ProjectDependencies) = postProject
8 | {5D469F03-878B-4652-AF39-20434C1EC28C} = {5D469F03-878B-4652-AF39-20434C1EC28C}
9 | EndProjectSection
10 | EndProject
11 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Cafe.Matcha.Packer", "Cafe.Matcha.Packer\Cafe.Matcha.Packer.csproj", "{5D469F03-878B-4652-AF39-20434C1EC28C}"
12 | EndProject
13 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{3137D877-9B3A-4F11-AF5E-9F882D92A378}"
14 | ProjectSection(SolutionItems) = preProject
15 | stylecop.json = stylecop.json
16 | StyleCop.ruleset = StyleCop.ruleset
17 | EndProjectSection
18 | EndProject
19 | Global
20 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
21 | Debug|Any CPU = Debug|Any CPU
22 | Debug|x64 = Debug|x64
23 | Release|Any CPU = Release|Any CPU
24 | Release|x64 = Release|x64
25 | EndGlobalSection
26 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
27 | {0A8BC2CF-BCBA-4A7F-974A-11F078BED997}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
28 | {0A8BC2CF-BCBA-4A7F-974A-11F078BED997}.Debug|Any CPU.Build.0 = Debug|Any CPU
29 | {0A8BC2CF-BCBA-4A7F-974A-11F078BED997}.Debug|x64.ActiveCfg = Debug|Any CPU
30 | {0A8BC2CF-BCBA-4A7F-974A-11F078BED997}.Debug|x64.Build.0 = Debug|Any CPU
31 | {0A8BC2CF-BCBA-4A7F-974A-11F078BED997}.Release|Any CPU.ActiveCfg = Release|Any CPU
32 | {0A8BC2CF-BCBA-4A7F-974A-11F078BED997}.Release|Any CPU.Build.0 = Release|Any CPU
33 | {0A8BC2CF-BCBA-4A7F-974A-11F078BED997}.Release|x64.ActiveCfg = Debug|Any CPU
34 | {0A8BC2CF-BCBA-4A7F-974A-11F078BED997}.Release|x64.Build.0 = Debug|Any CPU
35 | {5D469F03-878B-4652-AF39-20434C1EC28C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
36 | {5D469F03-878B-4652-AF39-20434C1EC28C}.Debug|Any CPU.Build.0 = Debug|Any CPU
37 | {5D469F03-878B-4652-AF39-20434C1EC28C}.Debug|x64.ActiveCfg = Debug|Any CPU
38 | {5D469F03-878B-4652-AF39-20434C1EC28C}.Debug|x64.Build.0 = Debug|Any CPU
39 | {5D469F03-878B-4652-AF39-20434C1EC28C}.Release|Any CPU.ActiveCfg = Release|Any CPU
40 | {5D469F03-878B-4652-AF39-20434C1EC28C}.Release|Any CPU.Build.0 = Release|Any CPU
41 | {5D469F03-878B-4652-AF39-20434C1EC28C}.Release|x64.ActiveCfg = Release|Any CPU
42 | {5D469F03-878B-4652-AF39-20434C1EC28C}.Release|x64.Build.0 = Release|Any CPU
43 | EndGlobalSection
44 | GlobalSection(SolutionProperties) = preSolution
45 | HideSolutionNode = FALSE
46 | EndGlobalSection
47 | GlobalSection(ExtensibilityGlobals) = postSolution
48 | SolutionGuid = {EFD171B9-8DC3-4CA1-98CD-47E42E86182F}
49 | EndGlobalSection
50 | EndGlobal
51 |
--------------------------------------------------------------------------------
/Cafe.Matcha/Utils/ParsePlugin.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) FFCafe. All rights reserved.
2 | // Licensed under the AGPL-3.0 license. See LICENSE file in the project root for full license information.
3 |
4 | namespace Cafe.Matcha.Utils
5 | {
6 | using Advanced_Combat_Tracker;
7 | using FFXIV_ACT_Plugin.Common;
8 |
9 | internal class ParsePlugin
10 | {
11 | public static ParsePlugin Instance { get; private set; } = null;
12 |
13 | public static void Init(IActPluginV1 plugin, Network.INetworkMonitor network)
14 | {
15 | Instance = new ParsePlugin(plugin, network);
16 | }
17 |
18 | private readonly FFXIV_ACT_Plugin.FFXIV_ACT_Plugin _parsePlugin;
19 |
20 | public Network.INetworkMonitor Network { private get; set; }
21 |
22 | public ParsePlugin(IActPluginV1 plugin, Network.INetworkMonitor network)
23 | {
24 | _parsePlugin = (FFXIV_ACT_Plugin.FFXIV_ACT_Plugin)plugin;
25 | Network = network;
26 | }
27 |
28 | public void Start()
29 | {
30 | _parsePlugin.DataSubscription.NetworkReceived += HandleMessageReceived;
31 | _parsePlugin.DataSubscription.NetworkSent += HandleMessageSent;
32 | }
33 |
34 | public void Stop()
35 | {
36 | _parsePlugin.DataSubscription.NetworkReceived -= HandleMessageReceived;
37 | _parsePlugin.DataSubscription.NetworkSent -= HandleMessageSent;
38 | }
39 |
40 | public Language GetLanguage()
41 | {
42 | return _parsePlugin.DataRepository.GetSelectedLanguageID();
43 | }
44 |
45 | public Constant.Region GetRegion()
46 | {
47 | var language = GetLanguage();
48 | switch (language)
49 | {
50 | case Language.Chinese:
51 | return Constant.Region.China;
52 | default:
53 | return Constant.Region.Global;
54 | }
55 | }
56 |
57 | public uint GetServer()
58 | {
59 | var combatantList = _parsePlugin.DataRepository.GetCombatantList();
60 | if (combatantList == null || combatantList.Count == 0)
61 | {
62 | return 0;
63 | }
64 |
65 | return combatantList[0].CurrentWorldID;
66 | }
67 |
68 | public uint GetCurrentTerritoryID()
69 | {
70 | return _parsePlugin.DataRepository.GetCurrentTerritoryID();
71 | }
72 |
73 | private void HandleMessageSent(string connection, long epoch, byte[] message)
74 | {
75 | Network?.HandleMessageSent(connection, epoch, message);
76 | }
77 |
78 | private void HandleMessageReceived(string connection, long epoch, byte[] message)
79 | {
80 | Network?.HandleMessageReceived(connection, epoch, message);
81 | }
82 | }
83 | }
84 |
--------------------------------------------------------------------------------
/Cafe.Matcha/Data.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) FFCafe. All rights reserved.
2 | // Licensed under the AGPL-3.0 license. See LICENSE file in the project root for full license information.
3 |
4 | namespace Cafe.Matcha
5 | {
6 | using System;
7 | using System.Collections.Generic;
8 | using System.IO;
9 | using System.Reflection;
10 | using System.Windows;
11 | using Cafe.Matcha.Utils;
12 | using Newtonsoft.Json;
13 |
14 | public class Data : StaticBindingTarget
15 | {
16 | #if GLOBAL
17 | public const string Title = "Matcha";
18 | #else
19 | public const string Title = "抹茶 Matcha";
20 | #endif
21 | public static string Version = Assembly.GetExecutingAssembly().GetName().Version.ToString();
22 |
23 | public Dictionary Fates { get; set; }
24 | public Dictionary DynamicEvents;
25 | public Dictionary Instances;
26 | public Dictionary InstanceTypes;
27 | public Dictionary Territories;
28 | public Dictionary Patches;
29 | public Dictionary Worlds;
30 | public List Templates { get; set; }
31 | public Dictionary Roulettes;
32 |
33 | private bool ReadData(string path, string file, out T dict) where T : new()
34 | {
35 | try
36 | {
37 | string content = File.ReadAllText(Path.Combine(path, file));
38 | dict = JsonConvert.DeserializeObject(content);
39 | return true;
40 | }
41 | catch
42 | {
43 | MessageBox.Show(string.Format("无法找到数据文件 {0} 或读取时发生错误,请检查是否对插件目录进行过修改,或尝试重新安装本插件。", file), Data.Title);
44 | dict = new T();
45 | return false;
46 | }
47 | }
48 |
49 | public void Init()
50 | {
51 | var dataRoot = Path.Combine(Helper.GetPluginDir(), "data");
52 |
53 | ReadData(dataRoot, "instance.json", out Instances);
54 | ReadData(dataRoot, "type.json", out InstanceTypes);
55 | ReadData(dataRoot, "territory.json", out Territories);
56 | ReadData(dataRoot, "roulette.json", out Roulettes);
57 | ReadData(dataRoot, "patch.json", out Patches);
58 | ReadData(dataRoot, "world.json", out Worlds);
59 | ReadData(dataRoot, "dynamic-event.json", out DynamicEvents);
60 |
61 | ReadData(dataRoot, "fate.json", out Dictionary fates);
62 | Fates = fates;
63 | ReadData(dataRoot, "template.json", out List templates);
64 | Templates = templates;
65 |
66 | IsLoaded = true;
67 | DataLoaded?.Invoke(this, EventArgs.Empty);
68 | }
69 |
70 | public bool IsLoaded = false;
71 | public event EventHandler DataLoaded;
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/Cafe.Matcha/data/template.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "name": {
4 | "chs": "«火天文书»第一卷",
5 | "en": "copy of the Book of Skyfire I",
6 | "ja": "黄道十二文書:炎天一巻",
7 | "de": "Tafel[p] des Himmelsfeuers I",
8 | "fr": "exemplaire du Livre du feu céleste I"
9 | },
10 | "fate": [
11 | 480,
12 | 589,
13 | 611
14 | ]
15 | },
16 | {
17 | "name": {
18 | "chs": "«火天文书»第二卷",
19 | "en": "copy of the Book of Skyfire II",
20 | "ja": "黄道十二文書:炎天二巻",
21 | "de": "Tafel[p] des Himmelsfeuers II",
22 | "fr": "exemplaire du Livre du feu céleste II"
23 | },
24 | "fate": [
25 | 571,
26 | 424,
27 | 633
28 | ]
29 | },
30 | {
31 | "name": {
32 | "chs": "«火狱文书»第一卷",
33 | "en": "copy of the Book of Netherfire I",
34 | "ja": "黄道十二文書:炎獄一巻",
35 | "de": "Tafel[p] des Jenseitsfeuers I",
36 | "fr": "exemplaire du Livre du feu infernal I"
37 | },
38 | "fate": [
39 | 521,
40 | 620,
41 | 430
42 | ]
43 | },
44 | {
45 | "name": {
46 | "chs": "«水天文书»第一卷",
47 | "en": "copy of the Book of Skyfall I",
48 | "ja": "黄道十二文書:水天一巻",
49 | "de": "Tafel[p] des Himmelsfalles I",
50 | "fr": "exemplaire du Livre de l'eau céleste I"
51 | },
52 | "fate": [
53 | 475,
54 | 577,
55 | 540
56 | ]
57 | },
58 | {
59 | "name": {
60 | "chs": "«水天文书»第二卷",
61 | "en": "copy of the Book of Skyfall II",
62 | "ja": "黄道十二文書:水天二巻",
63 | "de": "Tafel[p] des Himmelsfalles II",
64 | "fr": "exemplaire du Livre de l'eau céleste II"
65 | },
66 | "fate": [
67 | 569,
68 | 516,
69 | 616
70 | ]
71 | },
72 | {
73 | "name": {
74 | "chs": "«水狱文书»第一卷",
75 | "en": "copy of the Book of Netherfall I",
76 | "ja": "黄道十二文書:水獄一巻",
77 | "de": "Tafel[p] des Jenseitsfalles I",
78 | "fr": "exemplaire du Livre de l'eau infernale I"
79 | },
80 | "fate": [
81 | 632,
82 | 642,
83 | 499
84 | ]
85 | },
86 | {
87 | "name": {
88 | "chs": "«风天文书»第一卷",
89 | "en": "copy of the Book of Skywind I",
90 | "ja": "黄道十二文書:風天一巻",
91 | "de": "Tafel[p] des Himmelswindes I",
92 | "fr": "exemplaire du Livre du vent céleste I"
93 | },
94 | "fate": [
95 | 317,
96 | 604,
97 | 517
98 | ]
99 | },
100 | {
101 | "name": {
102 | "chs": "«风天文书»第二卷",
103 | "en": "copy of the Book of Skywind II",
104 | "ja": "黄道十二文書:風天二巻",
105 | "de": "Tafel[p] des Himmelswindes II",
106 | "fr": "exemplaire du Livre du vent céleste II"
107 | },
108 | "fate": [
109 | 552,
110 | 486,
111 | 628
112 | ]
113 | },
114 | {
115 | "name": {
116 | "chs": "«土天文书»第一卷",
117 | "en": "copy of the Book of Skyearth I",
118 | "ja": "黄道十二文書:土天一巻",
119 | "de": "Tafel[p] der Himmelserde I",
120 | "fr": "exemplaire du Livre de la terre céleste I"
121 | },
122 | "fate": [
123 | 493,
124 | 587,
125 | 543
126 | ]
127 | }
128 | ]
--------------------------------------------------------------------------------
/.github/workflows/build.yml:
--------------------------------------------------------------------------------
1 | name: Build
2 |
3 | on:
4 | push:
5 | branches: [master, staging]
6 | pull_request:
7 | branches: [master]
8 |
9 | jobs:
10 | build:
11 | strategy:
12 | matrix:
13 | configuration: [Release]
14 |
15 | runs-on: windows-latest
16 |
17 | env:
18 | Thirdparty_ACT: https://github.com/EQAditu/AdvancedCombatTracker/releases/download/3.6.0.275/ACTv3.zip
19 | Thirdparty_FFXIV_ACT_Plugin: https://github.com/ravahn/FFXIV_ACT_Plugin/raw/master/Releases/FFXIV_ACT_Plugin_SDK_2.0.7.0.zip
20 | Solution_Name: Cafe.Matcha.sln
21 | Secret_Path: Cafe.Matcha\Constant\Secret.Local.cs
22 |
23 | steps:
24 | - name: Checkout
25 | uses: actions/checkout@v4
26 | with:
27 | fetch-depth: 0
28 |
29 | - uses: actions/cache@v4
30 | with:
31 | path: ~/.nuget/packages
32 | key: ${{ runner.os }}-nuget-${{ hashFiles('**/*.csproj') }}
33 | restore-keys: |
34 | ${{ runner.os }}-nuget-
35 |
36 | - name: Setup NuGet.exe for use with actions
37 | uses: NuGet/setup-nuget@v2
38 | - name: Setup MSBuild.exe
39 | uses: microsoft/setup-msbuild@v2
40 |
41 | - name: Setup thirdparty dependencies
42 | run: |
43 | nuget restore ${{ env.Solution_Name }}
44 |
45 | New-Item -Name "thirdparty" -ItemType "directory" -Force
46 |
47 | Invoke-WebRequest -Uri ${{ env.Thirdparty_ACT }} -OutFile thirdparty\ACT.zip
48 | Expand-Archive thirdparty\ACT.zip -DestinationPath thirdparty\ACT -Force
49 |
50 | Invoke-WebRequest -Uri ${{ env.Thirdparty_FFXIV_ACT_Plugin }} -OutFile thirdparty\FFXIV_ACT_Plugin.zip
51 | Expand-Archive thirdparty\FFXIV_ACT_Plugin.zip -DestinationPath thirdparty\FFXIV_ACT_Plugin -Force
52 |
53 | Get-ChildItem .\thirdparty
54 | Get-ChildItem .\thirdparty\ACT
55 | Get-ChildItem .\thirdparty\FFXIV_ACT_Plugin
56 |
57 | - name: Write Secrets
58 | run: |
59 | $secret_content = [System.Convert]::FromBase64String("${{ secrets.CONSTANT_SECRET_LOCAL }}")
60 | [IO.File]::WriteAllBytes("${{ env.Secret_Path }}", $secret_content)
61 |
62 | - name: Build
63 | run: msbuild $env:Solution_Name /p:Configuration=$env:Configuration
64 | env:
65 | Configuration: ${{ matrix.configuration }}
66 |
67 | - name: Remove Secrets
68 | run: Remove-Item -path ${{ env.Secret_Path }}
69 |
70 | - name: Upload build artifacts
71 | uses: actions/upload-artifact@v4
72 | with:
73 | name: Bundle
74 | path: bin\*.zip
75 |
76 | - name: Release
77 | uses: softprops/action-gh-release@v1
78 | if: startsWith(github.ref, 'refs/tags/')
79 | with:
80 | files: bin\*.zip
81 | env:
82 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
83 |
84 | - name: Telegram Notify
85 | if: github.ref == 'refs/heads/master'
86 | shell: bash
87 | run: |
88 | export FILENAME=`cd bin && ls *.zip`
89 | curl -X POST -F chat_id=${{ secrets.TELEGRAM_CHAT }} -F "document=@\"bin/${FILENAME}\"" https://api.telegram.org/bot${{ secrets.TELEGRAM_TOKEN }}/sendDocument
90 |
--------------------------------------------------------------------------------
/Cafe.Matcha/Telemetry/Fate.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) FFCafe. All rights reserved.
2 | // Licensed under the AGPL-3.0 license. See LICENSE file in the project root for full license information.
3 |
4 | namespace Cafe.Matcha.Telemetry
5 | {
6 | using Cafe.Matcha.Models;
7 | using Cafe.Matcha.Utils;
8 | using Newtonsoft.Json;
9 |
10 | internal class FateDTO : TelemetryData
11 | {
12 | public FateDTO(uint fateId, Network.FateState state)
13 | {
14 | this.FateId = fateId;
15 | this.StartTime = state.StartTime;
16 | this.Duration = state.Duration;
17 | this.Progress = state.Progress;
18 | }
19 |
20 | [JsonProperty("fate")]
21 | public uint FateId = 0;
22 |
23 | [JsonProperty("start_time")]
24 | public uint StartTime = 0;
25 |
26 | [JsonProperty("duration")]
27 | public uint Duration = 0;
28 |
29 | [JsonProperty("progress")]
30 | public int Progress = 0;
31 |
32 | public bool Equals(FateDTO fateInit)
33 | {
34 | return base.Equals(fateInit) && FateId == fateInit.FateId && StartTime == fateInit.StartTime
35 | && Duration == fateInit.Duration && Progress == fateInit.Progress;
36 | }
37 |
38 | public override bool Equals(object obj)
39 | {
40 | if (ReferenceEquals(null, obj))
41 | {
42 | return false;
43 | }
44 |
45 | if (ReferenceEquals(this, obj))
46 | {
47 | return true;
48 | }
49 |
50 | return obj.GetType() == GetType() && Equals((FateDTO)obj);
51 | }
52 |
53 | public override int GetHashCode()
54 | {
55 | return base.GetHashCode() ^ FateId.GetHashCode() ^ StartTime.GetHashCode() ^ Duration.GetHashCode() ^ Progress.GetHashCode();
56 | }
57 |
58 | public override string ToString()
59 | {
60 | return $"Fate {{ Id={FateId}, StartTime={StartTime}, Duration={Duration}, Progress={Progress} }} {base.ToString()}";
61 | }
62 |
63 | public override bool TryMerge(TelemetryData data)
64 | {
65 | if (data is FateDTO fate && fate.FateId == FateId)
66 | {
67 | Merge(data);
68 |
69 | if (fate.StartTime != 0)
70 | {
71 | StartTime = fate.StartTime;
72 | }
73 |
74 | if (fate.Duration != 0)
75 | {
76 | Duration = fate.Duration;
77 | }
78 |
79 | if (fate.Progress != 0)
80 | {
81 | Progress = fate.Progress;
82 | }
83 |
84 | return true;
85 | }
86 |
87 | return false;
88 | }
89 | }
90 |
91 | internal class Fate : TelemetryWorker
92 | {
93 | public Fate() : base(Constant.Secret.TelemetryFate) { }
94 |
95 | public void Send(uint fateId, Network.FateState state)
96 | {
97 | Send(new FateDTO(fateId, state));
98 | }
99 | }
100 | }
101 |
--------------------------------------------------------------------------------
/Cafe.Matcha/Network/Universalis/Types/UniversalisItemListingsEntry.cs:
--------------------------------------------------------------------------------
1 | namespace Cafe.Matcha.Network.Universalis
2 | {
3 | using System.Collections.Generic;
4 | using Newtonsoft.Json;
5 |
6 | ///
7 | /// A Universalis API structure.
8 | ///
9 | internal class UniversalisItemListingsEntry
10 | {
11 | ///
12 | /// Gets or sets the listing ID.
13 | ///
14 | [JsonProperty("listingID")]
15 | public string ListingId { get; set; }
16 |
17 | ///
18 | /// Gets or sets a value indicating whether the item is HQ.
19 | ///
20 | [JsonProperty("hq")]
21 | public bool Hq { get; set; }
22 |
23 | ///
24 | /// Gets or sets the item price per unit.
25 | ///
26 | [JsonProperty("pricePerUnit")]
27 | public uint PricePerUnit { get; set; }
28 |
29 | ///
30 | /// Gets or sets the item quantity.
31 | ///
32 | [JsonProperty("quantity")]
33 | public uint Quantity { get; set; }
34 |
35 | ///
36 | /// Gets or sets the name of the retainer selling the item.
37 | ///
38 | [JsonProperty("retainerName")]
39 | public string RetainerName { get; set; }
40 |
41 | ///
42 | /// Gets or sets the ID of the retainer selling the item.
43 | ///
44 | [JsonProperty("retainerID")]
45 | public string RetainerId { get; set; }
46 |
47 | ///
48 | /// Gets or sets the name of the user who created the entry.
49 | ///
50 | [JsonProperty("creatorName")]
51 | public string CreatorName { get; set; }
52 |
53 | ///
54 | /// Gets or sets a value indicating whether the item is on a mannequin.
55 | ///
56 | [JsonProperty("onMannequin")]
57 | public bool OnMannequin { get; set; }
58 |
59 | ///
60 | /// Gets or sets the seller ID.
61 | ///
62 | [JsonProperty("sellerID")]
63 | public string SellerId { get; set; }
64 |
65 | ///
66 | /// Gets or sets the ID of the user who created the entry.
67 | ///
68 | [JsonProperty("creatorID")]
69 | public string CreatorId { get; set; }
70 |
71 | ///
72 | /// Gets or sets the ID of the dye on the item.
73 | ///
74 | [JsonProperty("stainID")]
75 | public int StainId { get; set; }
76 |
77 | ///
78 | /// Gets or sets the city where the selling retainer resides.
79 | ///
80 | [JsonProperty("retainerCity")]
81 | public int RetainerCity { get; set; }
82 |
83 | ///
84 | /// Gets or sets the last time the entry was reviewed.
85 | ///
86 | [JsonProperty("lastReviewTime")]
87 | public long LastReviewTime { get; set; }
88 |
89 | ///
90 | /// Gets or sets the materia attached to the item.
91 | ///
92 | [JsonProperty("materia")]
93 | public List Materia { get; set; }
94 | }
95 | }
96 |
--------------------------------------------------------------------------------
/Cafe.Matcha/Network/Structures/Examine.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) FFCafe. All rights reserved.
2 | // Licensed under the AGPL-3.0 license. See LICENSE file in the project root for full license information.
3 |
4 | namespace Cafe.Matcha.Network.Structures
5 | {
6 | using System.Collections.Generic;
7 | using System.IO;
8 | using System.Text;
9 |
10 | ///
11 | /// This class represents the "Result Dialog" packet. This is also used e.g. for reduction results, but we only care about tax rates.
12 | /// We can do that by checking the "Category" field.
13 | ///
14 | public class Examine
15 | {
16 | public class Gear
17 | {
18 | public uint ItemId { get; internal set; }
19 | public uint Glamour { get; internal set; }
20 | public bool HQ { get; internal set; }
21 | public List Materias { get; internal set; }
22 | }
23 |
24 | public byte ClassJob { get; internal set; }
25 | public byte Level { get; internal set; }
26 | public ushort WorldId { get; internal set; }
27 | public List Gears { get; internal set; }
28 | public string Name { get; internal set; }
29 |
30 | ///
31 | /// Read a object from memory.
32 | ///
33 | /// Data to read.
34 | /// A new object.
35 | public static Examine Read(byte[] data)
36 | {
37 | using (var stream = new MemoryStream(data))
38 | {
39 | using (var reader = new BinaryReader(stream))
40 | {
41 | var output = new Examine();
42 |
43 | stream.Position += 2;
44 | output.ClassJob = reader.ReadByte();
45 | output.Level = reader.ReadByte();
46 | stream.Position += 0x2E;
47 | output.WorldId = reader.ReadUInt16();
48 | stream.Position = 0x50;
49 |
50 | output.Gears = new List();
51 | for (int i = 0; i < 14; i++)
52 | {
53 | var gear = new Gear();
54 | gear.ItemId = reader.ReadUInt32();
55 | gear.Glamour = reader.ReadUInt32();
56 | stream.Position += 8;
57 | gear.HQ = reader.ReadBoolean();
58 | stream.Position += 1;
59 |
60 | gear.Materias = new List();
61 | for (int j = 0; j < 5; j++)
62 | {
63 | var materia = new Materia();
64 | materia.Type = reader.ReadUInt16();
65 | materia.Tier = reader.ReadUInt16();
66 | gear.Materias.Add(materia);
67 | }
68 |
69 | output.Gears.Add(gear);
70 | stream.Position += 2;
71 | }
72 |
73 | output.Name = Encoding.UTF8.GetString(reader.ReadBytes(0x20)).TrimEnd('\u0000');
74 | return output;
75 | }
76 | }
77 | }
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/Cafe.Matcha/Network/Structures/MarketBoardPurchaseHandler.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) FFCafe. All rights reserved.
2 | // Licensed under the AGPL-3.0 license. See LICENSE file in the project root for full license information.
3 |
4 | namespace Cafe.Matcha.Network.Structures
5 | {
6 | using System.IO;
7 |
8 | ///
9 | /// Represents market board purchase information. This message is sent from the
10 | /// client when a purchase is made at a market board.
11 | ///
12 | public class MarketBoardPurchaseHandler : IMarketBoardPurchaseHandler
13 | {
14 | private MarketBoardPurchaseHandler()
15 | {
16 | }
17 |
18 | ///
19 | /// Gets the object ID of the retainer associated with the sale.
20 | ///
21 | public ulong RetainerId { get; private set; }
22 |
23 | ///
24 | /// Gets the object ID of the item listing.
25 | ///
26 | public ulong ListingId { get; private set; }
27 |
28 | ///
29 | /// Gets the item ID of the item that was purchased.
30 | ///
31 | public uint CatalogId { get; private set; }
32 |
33 | ///
34 | /// Gets the quantity of the item that was purchased.
35 | ///
36 | public uint ItemQuantity { get; private set; }
37 |
38 | ///
39 | /// Gets the unit price of the item.
40 | ///
41 | public uint PricePerUnit { get; private set; }
42 |
43 | ///
44 | /// Gets a value indicating whether the item is HQ.
45 | ///
46 | public bool IsHq { get; private set; }
47 |
48 | ///
49 | /// Gets the total tax.
50 | ///
51 | public uint TotalTax { get; private set; }
52 |
53 | ///
54 | /// Gets the city ID of the retainer selling the item.
55 | ///
56 | public int RetainerCityId { get; private set; }
57 |
58 | ///
59 | /// Reads market board purchase information from the struct at the provided pointer.
60 | ///
61 | /// Data to read.
62 | /// An object representing the data read.
63 | public static MarketBoardPurchaseHandler Read(byte[] data)
64 | {
65 | using (var stream = new MemoryStream(data))
66 | {
67 | using (var reader = new BinaryReader(stream))
68 | {
69 | var output = new MarketBoardPurchaseHandler();
70 |
71 | output.RetainerId = reader.ReadUInt64();
72 | output.ListingId = reader.ReadUInt64();
73 |
74 | output.CatalogId = reader.ReadUInt32();
75 | output.ItemQuantity = reader.ReadUInt32();
76 | output.PricePerUnit = reader.ReadUInt32();
77 | output.TotalTax = reader.ReadUInt32();
78 |
79 | reader.ReadUInt16(); // Slot
80 |
81 | output.IsHq = reader.ReadBoolean();
82 | output.RetainerCityId = reader.ReadByte();
83 |
84 | return output;
85 | }
86 | }
87 | }
88 | }
89 | }
90 |
--------------------------------------------------------------------------------
/Cafe.Matcha/Constant/MatchaOpcode.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) FFCafe. All rights reserved.
2 | // Licensed under the AGPL-3.0 license. See LICENSE file in the project root for full license information.
3 |
4 | namespace Cafe.Matcha.Constant
5 | {
6 | using System.Collections.Generic;
7 |
8 | internal enum MatchaOpcode
9 | {
10 | ActorControl,
11 | ActorControlSelf,
12 | CEDirector,
13 | CompanyAirshipStatus,
14 | CompanySubmersibleStatus,
15 | ContentFinderNotifyPop,
16 | ResumeEventScene32,
17 | EventPlay,
18 | EventStart,
19 | Examine,
20 | FateInfo,
21 | InitZone,
22 | InventoryTransaction,
23 | ItemInfo,
24 | MarketBoardItemListing,
25 | MarketBoardItemListingCount,
26 | MarketBoardItemListingHistory,
27 | MarketBoardRequestItemListingInfo,
28 | NpcSpawn,
29 | PlayerSetup,
30 | PlayerSpawn,
31 | WorldVisitQueue,
32 | }
33 |
34 | internal static class OpcodeStorage
35 | {
36 | public static Dictionary Global = new Dictionary
37 | {
38 | { 0x00a1, MatchaOpcode.ActorControl },
39 | { 0x03bd, MatchaOpcode.ActorControlSelf },
40 | { 0xf002, MatchaOpcode.CEDirector },
41 | { 0x02ee, MatchaOpcode.CompanyAirshipStatus },
42 | { 0x01ad, MatchaOpcode.CompanySubmersibleStatus },
43 | { 0x0387, MatchaOpcode.ContentFinderNotifyPop },
44 | { 0xf006, MatchaOpcode.ResumeEventScene32 },
45 | { 0x0083, MatchaOpcode.EventPlay },
46 | { 0x0107, MatchaOpcode.EventStart },
47 | { 0x0349, MatchaOpcode.Examine },
48 | { 0xf00a, MatchaOpcode.FateInfo },
49 | { 0x03a3, MatchaOpcode.InitZone },
50 | { 0x00eb, MatchaOpcode.InventoryTransaction },
51 | { 0x00f6, MatchaOpcode.ItemInfo },
52 | { 0x027d, MatchaOpcode.MarketBoardItemListing },
53 | { 0x011e, MatchaOpcode.MarketBoardItemListingCount },
54 | { 0x03c1, MatchaOpcode.MarketBoardItemListingHistory },
55 | { 0xf011, MatchaOpcode.MarketBoardRequestItemListingInfo },
56 | { 0x02ea, MatchaOpcode.NpcSpawn },
57 | { 0x026f, MatchaOpcode.PlayerSetup },
58 | { 0x00e3, MatchaOpcode.PlayerSpawn },
59 | { 0xf015, MatchaOpcode.WorldVisitQueue },
60 | };
61 | public static Dictionary China = new Dictionary
62 | {
63 | { 0x0167, MatchaOpcode.ActorControl },
64 | { 0x0110, MatchaOpcode.ActorControlSelf },
65 | { 0x0364, MatchaOpcode.CEDirector },
66 | { 0x02f4, MatchaOpcode.CompanyAirshipStatus },
67 | { 0x00e5, MatchaOpcode.CompanySubmersibleStatus },
68 | { 0x0080, MatchaOpcode.ContentFinderNotifyPop },
69 | { 0x024e, MatchaOpcode.ResumeEventScene32 },
70 | { 0x03b5, MatchaOpcode.EventPlay },
71 | { 0x0235, MatchaOpcode.EventStart },
72 | { 0x0340, MatchaOpcode.Examine },
73 | { 0x02e5, MatchaOpcode.FateInfo },
74 | { 0x0362, MatchaOpcode.InitZone },
75 | { 0x01a0, MatchaOpcode.InventoryTransaction },
76 | { 0x00b7, MatchaOpcode.ItemInfo },
77 | { 0x02c1, MatchaOpcode.MarketBoardItemListing },
78 | { 0x013f, MatchaOpcode.MarketBoardItemListingCount },
79 | { 0x02ed, MatchaOpcode.MarketBoardItemListingHistory },
80 | { 0x8308, MatchaOpcode.MarketBoardRequestItemListingInfo },
81 | { 0x00fc, MatchaOpcode.NpcSpawn },
82 | { 0x0300, MatchaOpcode.PlayerSetup },
83 | { 0x03e7, MatchaOpcode.PlayerSpawn },
84 | { 0x03af, MatchaOpcode.WorldVisitQueue },
85 | };
86 | }
87 | }
88 |
--------------------------------------------------------------------------------
/Cafe.Matcha/Network/Structures/IMarketBoardCurrentOfferings.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) FFCafe. All rights reserved.
2 | // Licensed under the AGPL-3.0 license. See LICENSE file in the project root for full license information.
3 |
4 | namespace Cafe.Matcha.Network.Structures
5 | {
6 | using System.Collections.Generic;
7 |
8 | ///
9 | /// An interface that represents the current market board offerings.
10 | ///
11 | public interface IMarketBoardCurrentOfferings
12 | {
13 | ///
14 | /// Gets the list of individual item listings.
15 | ///
16 | IReadOnlyList ItemListings { get; }
17 |
18 | ///
19 | /// Gets the request ID.
20 | ///
21 | int RequestId { get; }
22 | }
23 |
24 | ///
25 | /// An interface that represents the current market board offering of a single item from the .
26 | ///
27 | public interface IMarketBoardItemListing
28 | {
29 | ///
30 | /// Gets the artisan ID.
31 | ///
32 | ulong ArtisanId { get; }
33 |
34 | ///
35 | /// Gets the item ID.
36 | ///
37 | uint ItemId { get; }
38 |
39 | ///
40 | /// Gets a value indicating whether the item is HQ.
41 | ///
42 | bool IsHq { get; }
43 |
44 | ///
45 | /// Gets the item quantity.
46 | ///
47 | uint ItemQuantity { get; }
48 |
49 | ///
50 | /// Gets the listing ID.
51 | ///
52 | ulong ListingId { get; }
53 |
54 | ///
55 | /// Gets the list of materia attached to this item.
56 | ///
57 | IReadOnlyList Materia { get; }
58 |
59 | ///
60 | /// Gets the amount of attached materia.
61 | ///
62 | int MateriaCount { get; }
63 |
64 | ///
65 | /// Gets a value indicating whether this item is on a mannequin.
66 | ///
67 | bool OnMannequin { get; }
68 |
69 | ///
70 | /// Gets the price per unit.
71 | ///
72 | uint PricePerUnit { get; }
73 |
74 | ///
75 | /// Gets the city ID of the retainer selling the item.
76 | ///
77 | int RetainerCityId { get; }
78 |
79 | ///
80 | /// Gets the ID of the retainer selling the item.
81 | ///
82 | ulong RetainerId { get; }
83 |
84 | ///
85 | /// Gets the name of the retainer.
86 | ///
87 | string RetainerName { get; }
88 |
89 | ///
90 | /// Gets the first stain or applied dye of the item.
91 | ///
92 | int Stain1Id { get; }
93 |
94 | ///
95 | /// Gets the second stain or applied dye of the item.
96 | ///
97 | int Stain2Id { get; }
98 |
99 | ///
100 | /// Gets the total tax.
101 | ///
102 | uint TotalTax { get; }
103 | }
104 |
105 | ///
106 | /// An interface that represents the materia slotted to an .
107 | ///
108 | public interface IItemMateria
109 | {
110 | ///
111 | /// Gets the materia index.
112 | ///
113 | int Index { get; }
114 |
115 | ///
116 | /// Gets the materia ID.
117 | ///
118 | int MateriaId { get; }
119 | }
120 | }
121 |
--------------------------------------------------------------------------------
/Cafe.Matcha/Network/Packet.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) FFCafe. All rights reserved.
2 | // Licensed under the AGPL-3.0 license. See LICENSE file in the project root for full license information.
3 |
4 | namespace Cafe.Matcha.Network
5 | {
6 | using System;
7 | using System.Linq;
8 | using Cafe.Matcha.Constant;
9 |
10 | internal class Packet
11 | {
12 | private const int HeaderLength = 32;
13 |
14 | ///
15 | /// Raw packet, including headers.
16 | ///
17 | public byte[] Bytes;
18 |
19 | ///
20 | /// Is this packet valid.
21 | ///
22 | public bool Valid = false;
23 |
24 | ///
25 | /// Is this packet known.
26 | ///
27 | public bool Known = false;
28 |
29 | ///
30 | /// SegmentType.
31 | ///
32 | public byte SegmentType;
33 |
34 | ///
35 | /// Sender.
36 | ///
37 | public PacketSender Sender;
38 |
39 | ///
40 | /// Source.
41 | ///
42 | public uint Source;
43 |
44 | ///
45 | /// Target.
46 | ///
47 | public uint Target;
48 |
49 | ///
50 | /// Opcode.
51 | ///
52 | public ushort Opcode;
53 |
54 | ///
55 | /// Opcode.
56 | ///
57 | public MatchaOpcode MatchaOpcode;
58 |
59 | ///
60 | /// Gets packet length, including headers.
61 | ///
62 | public int Length => Bytes.Length;
63 |
64 | public int DataLength => Bytes.Length - HeaderLength;
65 |
66 | public Packet(PacketSender sender, byte[] bytes)
67 | {
68 | Sender = sender;
69 | Bytes = bytes;
70 |
71 | if (bytes.Length < HeaderLength)
72 | {
73 | return;
74 | }
75 |
76 | // Deucalion gives wrong type (0)
77 | SegmentType = bytes[12];
78 | if (SegmentType != 0 && SegmentType != 3)
79 | {
80 | return;
81 | }
82 |
83 | Source = BitConverter.ToUInt32(Bytes, 4);
84 | Target = BitConverter.ToUInt32(Bytes, 8);
85 | Opcode = BitConverter.ToUInt16(Bytes, 18);
86 | Valid = true;
87 |
88 | Known = GetMatchaOpcode(out MatchaOpcode);
89 | }
90 |
91 | private bool GetMatchaOpcode(out MatchaOpcode matchaOpcode)
92 | {
93 | var key = Sender == PacketSender.Server ? Opcode : (ushort)(0x8000 | Opcode);
94 | var region = Config.Instance.Region;
95 | switch (region)
96 | {
97 | case Region.Global:
98 | return OpcodeStorage.Global.TryGetValue(key, out matchaOpcode);
99 |
100 | case Region.China:
101 | return OpcodeStorage.China.TryGetValue(key, out matchaOpcode);
102 |
103 | default:
104 | matchaOpcode = default;
105 | return false;
106 | }
107 | }
108 |
109 | public byte[] GetRawData()
110 | {
111 | return Bytes.Skip(HeaderLength).ToArray();
112 | }
113 |
114 | public uint ReadUInt16(int startIndex)
115 | {
116 | return BitConverter.ToUInt16(Bytes, startIndex + HeaderLength);
117 | }
118 |
119 | public uint ReadUInt32(int startIndex)
120 | {
121 | return BitConverter.ToUInt32(Bytes, startIndex + HeaderLength);
122 | }
123 |
124 | public enum PacketSender
125 | {
126 | Server,
127 | Client
128 | }
129 | }
130 | }
131 |
--------------------------------------------------------------------------------
/Cafe.Matcha/Utils/Telemetry.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) FFCafe. All rights reserved.
2 | // Licensed under the AGPL-3.0 license. See LICENSE file in the project root for full license information.
3 |
4 | namespace Cafe.Matcha.Utils
5 | {
6 | using System;
7 | using System.Collections.Generic;
8 | using System.Threading.Tasks;
9 | using System.Windows;
10 | using Cafe.Matcha.Constant;
11 | using Cafe.Matcha.Models;
12 | using Cafe.Matcha.Views;
13 |
14 | internal class TelemetryWorker where T : Models.TelemetryData
15 | {
16 | protected string bucketId;
17 | /**
18 | * Aggregation time in milliseconds
19 | */
20 | protected int aggregationTime = 10000;
21 | protected List list = new List();
22 |
23 | private object listLock = new object();
24 |
25 | private Task task = null;
26 |
27 | public TelemetryWorker(string bucketId)
28 | {
29 | this.bucketId = bucketId;
30 | }
31 |
32 | public void Send(T item)
33 | {
34 | lock (listLock)
35 | {
36 | foreach (var oldItem in list)
37 | {
38 | if (oldItem.TryMerge(item))
39 | {
40 | #if DEBUG
41 | Log.Warn(LogType.Telemetry, $"{GetType().Name} Merged to {oldItem}");
42 | #endif
43 | return;
44 | }
45 | }
46 |
47 | #if DEBUG
48 | Log.Warn(LogType.Telemetry, $"{GetType().Name} Adding {item}");
49 | #endif
50 | list.Add(item);
51 | }
52 |
53 | if (task == null)
54 | {
55 | task = new Task(async () =>
56 | {
57 | await Task.Delay(aggregationTime);
58 |
59 | List list;
60 | lock (listLock)
61 | {
62 | list = new List(this.list);
63 | this.list.Clear();
64 | task = null;
65 | }
66 |
67 | if (list.Count > 0)
68 | {
69 | Telemetry.Instance.Send(bucketId, list);
70 | }
71 | });
72 |
73 | task.Start();
74 | }
75 | }
76 | }
77 |
78 | internal class Telemetry
79 | {
80 | public static Telemetry Instance { get; private set; } = null;
81 | public static void Init()
82 | {
83 | Instance = new Telemetry();
84 | }
85 |
86 | public bool Enabled
87 | {
88 | get
89 | {
90 | var hasUUID = Config.UUID != null && Config.UUID != "no";
91 | return Config.Enable && hasUUID && !string.IsNullOrEmpty(Secret.TelemetryRoot);
92 | }
93 | set
94 | {
95 | if (value)
96 | {
97 | Config.Enable = true;
98 | Config.Agreement = CurrentAgreement;
99 | if (string.IsNullOrEmpty(Config.UUID))
100 | {
101 | Config.UUID = Guid.NewGuid().ToString();
102 | }
103 | }
104 | else
105 | {
106 | Config.Enable = false;
107 | Config.Agreement = "no";
108 | Config.UUID = null;
109 | }
110 | }
111 | }
112 |
113 | private const string CurrentAgreement = "20220406";
114 |
115 | private ConfigTelemetry Config => Matcha.Config.Instance.Telemetry;
116 |
117 | public Telemetry()
118 | {
119 | if (Config.Agreement == null || (Config.Enable && Config.Agreement != CurrentAgreement))
120 | {
121 | var dialog = new TelemetrySetting(true);
122 | dialog.WindowStartupLocation = WindowStartupLocation.CenterScreen;
123 | dialog.Topmost = true;
124 | dialog.Show();
125 | }
126 | }
127 |
128 | public void Send(string bucketId, IEnumerable data)
129 | {
130 | if (!Enabled)
131 | {
132 | return;
133 | }
134 |
135 | #if DEBUG
136 | Log.Warn(LogType.Telemetry, $"Posting to {Secret.TelemetryRoot}/{bucketId}");
137 | #endif
138 | _ = Request.SendAsJson($"{Secret.TelemetryRoot}/{bucketId}/batch", "", data);
139 | }
140 | }
141 | }
142 |
--------------------------------------------------------------------------------
/Cafe.Matcha/Utils/Helper.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) FFCafe. All rights reserved.
2 | // Licensed under the AGPL-3.0 license. See LICENSE file in the project root for full license information.
3 |
4 | namespace Cafe.Matcha.Utils
5 | {
6 | using System;
7 | using System.IO;
8 | using System.Linq;
9 | using System.Reflection;
10 | using System.Runtime.InteropServices;
11 | using System.Security.Cryptography;
12 | using System.Text;
13 | using System.Threading.Tasks;
14 | using System.Windows;
15 | using System.Windows.Interop;
16 | using Advanced_Combat_Tracker;
17 |
18 | internal class Helper
19 | {
20 | private static readonly int[] WaitTime = { 5, 5, 10, 15, 25 };
21 | public static IActPluginV1 Instance = null;
22 | public static async Task GetFFXIVPlugin()
23 | {
24 | for (int i = 0; i < WaitTime.Length; ++i)
25 | {
26 | var plugin = ActGlobals.oFormActMain.ActPlugins.FirstOrDefault(x => x.cbEnabled.Checked && x.lblPluginTitle.Text == "FFXIV_ACT_Plugin.dll");
27 | if (plugin != null)
28 | {
29 | return plugin.pluginObj;
30 | }
31 |
32 | await Task.Delay(WaitTime[i] * 1000);
33 | }
34 |
35 | return null;
36 | }
37 |
38 | public static string GetConfigDir()
39 | {
40 | return Path.Combine(ActGlobals.oFormActMain.AppDataFolder.FullName, "Config");
41 | }
42 |
43 | public static string GetPluginDir()
44 | {
45 | if (Instance != null)
46 | {
47 | foreach (ActPluginData plugin in ActGlobals.oFormActMain.ActPlugins)
48 | {
49 | if (plugin.pluginObj == Instance)
50 | {
51 | return plugin.pluginFile.Directory.FullName;
52 | }
53 | }
54 | }
55 |
56 | foreach (ActPluginData plugin in ActGlobals.oFormActMain.ActPlugins)
57 | {
58 | if (plugin.pluginFile.Name == "Cafe.Matcha.dll")
59 | {
60 | return plugin.pluginFile.Directory.FullName;
61 | }
62 | }
63 |
64 | string libPath = Assembly.GetExecutingAssembly().Location;
65 | if (libPath == "")
66 | {
67 | throw new Exception("Failed to locate the library.");
68 | }
69 |
70 | return Path.GetDirectoryName(libPath);
71 | }
72 |
73 | public static void CheckLicenseNotice()
74 | {
75 | MD5 md5 = MD5.Create();
76 | var pathHash = md5.ComputeHash(Encoding.UTF8.GetBytes(GetPluginDir()));
77 | var hash = BitConverter.ToString(pathHash).Replace("-", "");
78 |
79 | if (hash != Config.Instance.Hash)
80 | {
81 | Config.Instance.Hash = hash;
82 | var dialog = new Views.License();
83 | dialog.WindowStartupLocation = WindowStartupLocation.CenterScreen;
84 | dialog.Topmost = true;
85 | dialog.ShowDialog();
86 | }
87 | }
88 |
89 | public static long Now
90 | {
91 | get
92 | {
93 | return DateTimeOffset.Now.ToUnixTimeMilliseconds();
94 | }
95 | }
96 |
97 | #pragma warning disable SA1310 // Field names should not contain underscore
98 | private const int GWL_STYLE = -16;
99 | private const int WS_SYSMENU = 0x80000;
100 | #pragma warning restore SA1310 // Field names should not contain underscore
101 |
102 | [DllImport("user32.dll")]
103 | private static extern int GetWindowLong(IntPtr hwnd, int index);
104 |
105 | [DllImport("user32.dll")]
106 | private static extern int SetWindowLong(IntPtr hwnd, int index, int newStyle);
107 |
108 | [DllImport("user32.dll")]
109 | private static extern bool SetWindowPos(IntPtr hwnd, IntPtr hwndInsertAfter,
110 | int x, int y, int width, int height, uint flags);
111 |
112 | public static void SetDialog(Window window)
113 | {
114 | var hwnd = new WindowInteropHelper(window).Handle;
115 | SetWindowLong(hwnd, GWL_STYLE, GetWindowLong(hwnd, GWL_STYLE) & ~WS_SYSMENU);
116 | }
117 |
118 | public static string ReadString(byte[] data, int offset, int length)
119 | {
120 | return Encoding.UTF8.GetString(data, offset, length).TrimEnd((char)0);
121 | }
122 | }
123 | }
124 |
--------------------------------------------------------------------------------
/Cafe.Matcha/Telemetry/Npc.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) FFCafe. All rights reserved.
2 | // Licensed under the AGPL-3.0 license. See LICENSE file in the project root for full license information.
3 |
4 | namespace Cafe.Matcha.Telemetry
5 | {
6 | using Cafe.Matcha.Models;
7 | using Cafe.Matcha.Utils;
8 | using Newtonsoft.Json;
9 |
10 | internal class NpcDTO : Models.TelemetryData
11 | {
12 | public NpcDTO(uint id, Network.NpcState state) : base()
13 | {
14 | this.CharacterID = id;
15 | this.BNpcName = state.BNpcName;
16 | this.Fate = state.Fate;
17 | this.X = state.Position.X;
18 | this.Y = state.Position.Y;
19 | this.Z = state.Position.Z;
20 | this.Level = state.Level;
21 | this.CurHP = state.CurHP;
22 | this.MaxHP = state.MaxHP;
23 | this.Location = state.Location;
24 | }
25 |
26 | [JsonProperty("cid")]
27 | public uint CharacterID = 0;
28 |
29 | [JsonProperty("npc")]
30 | public uint BNpcName = 0;
31 |
32 | [JsonProperty("fate")]
33 | public uint Fate = 0;
34 |
35 | [JsonProperty("x")]
36 | public float X = 0;
37 |
38 | [JsonProperty("y")]
39 | public float Y = 0;
40 |
41 | [JsonProperty("z")]
42 | public float Z = 0;
43 |
44 | [JsonProperty("level")]
45 | public ushort Level = 0;
46 |
47 | [JsonProperty("cur_hp")]
48 | public uint CurHP = 0;
49 |
50 | [JsonProperty("max_hp")]
51 | public uint MaxHP = 0;
52 |
53 | [JsonProperty("location")]
54 | public uint Location = 0;
55 |
56 | public bool Equals(NpcDTO npc)
57 | {
58 | return base.Equals(npc) && BNpcName == npc.BNpcName &&
59 | Fate == npc.Fate && Level == npc.Level &&
60 | CurHP == npc.CurHP && MaxHP == npc.MaxHP &&
61 | Location == npc.Location;
62 | }
63 |
64 | public override bool Equals(object obj)
65 | {
66 | if (ReferenceEquals(null, obj))
67 | {
68 | return false;
69 | }
70 |
71 | if (ReferenceEquals(this, obj))
72 | {
73 | return true;
74 | }
75 |
76 | return obj.GetType() == GetType() && Equals((NpcDTO)obj);
77 | }
78 |
79 | public override int GetHashCode()
80 | {
81 | return base.GetHashCode() ^ BNpcName.GetHashCode() ^ Fate.GetHashCode() ^ Level.GetHashCode()
82 | ^ CurHP.GetHashCode() ^ MaxHP.GetHashCode() ^ Location.GetHashCode();
83 | }
84 |
85 | public override string ToString()
86 | {
87 | return $"NpcSpawn {{ Id={BNpcName}, Zone={Zone}, Fate={Fate}, Level={Level}, HP={CurHP}/{MaxHP}, X={X}, Y={Y}, Z={Z}, Location={Location} }} {base.ToString()}";
88 | }
89 |
90 | public override bool TryMerge(TelemetryData data)
91 | {
92 | if (data is NpcDTO npc && npc.CharacterID == CharacterID)
93 | {
94 | Merge(data);
95 |
96 | if (npc.BNpcName != 0)
97 | {
98 | BNpcName = npc.BNpcName;
99 | }
100 |
101 | if (npc.Fate != 0)
102 | {
103 | Fate = npc.Fate;
104 | }
105 |
106 | if (npc.Level != 0)
107 | {
108 | Level = npc.Level;
109 | }
110 |
111 | if (npc.X != 0)
112 | {
113 | X = npc.X;
114 | }
115 |
116 | if (npc.Y != 0)
117 | {
118 | Y = npc.Y;
119 | }
120 |
121 | if (npc.Z != 0)
122 | {
123 | Z = npc.Z;
124 | }
125 |
126 | if (npc.CurHP != 0)
127 | {
128 | CurHP = npc.CurHP;
129 | }
130 |
131 | if (npc.MaxHP != 0)
132 | {
133 | MaxHP = npc.MaxHP;
134 | }
135 |
136 | if (npc.Location != 0)
137 | {
138 | Location = npc.Location;
139 | }
140 |
141 | return true;
142 | }
143 |
144 | return false;
145 | }
146 | }
147 |
148 | internal class Npc : TelemetryWorker
149 | {
150 | public Npc() : base(Constant.Secret.TelemetryNpc) { }
151 |
152 | public void Send(uint id, Network.NpcState state)
153 | {
154 | Send(new NpcDTO(id, state));
155 | }
156 | }
157 | }
--------------------------------------------------------------------------------
/Cafe.Matcha/Utils/Output.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) FFCafe. All rights reserved.
2 | // Licensed under the AGPL-3.0 license. See LICENSE file in the project root for full license information.
3 |
4 | namespace Cafe.Matcha.Utils
5 | {
6 | using System;
7 | using System.Collections.Generic;
8 | using System.Windows;
9 | using Advanced_Combat_Tracker;
10 | using Cafe.Matcha.Constant;
11 | using Cafe.Matcha.DTO;
12 | using Cafe.Matcha.Models;
13 | using Newtonsoft.Json.Linq;
14 | using Windows.Data.Xml.Dom;
15 | using Windows.UI.Notifications;
16 |
17 | internal class Output
18 | {
19 | private static Models.ConfigOutput Config => Matcha.Config.Instance.Output;
20 | private static bool Compat => Matcha.Config.Instance.Logger.Compat;
21 |
22 | private static void SendNativeToast(string message)
23 | {
24 | Version currentVersion = Environment.OSVersion.Version;
25 | Version compareToVersion = new Version("6.2");
26 | if (currentVersion.CompareTo(compareToVersion) >= 0)
27 | {
28 | XmlDocument toastXml = ToastNotificationManager.GetTemplateContent(ToastTemplateType.ToastText02);
29 | XmlNodeList stringElements = toastXml.GetElementsByTagName("text");
30 | for (int i = 0; i < stringElements.Length; i++)
31 | {
32 | stringElements[i].AppendChild(toastXml.CreateTextNode(message));
33 | }
34 |
35 | ToastNotification toast = new ToastNotification(toastXml);
36 | ToastNotificationManager.CreateToastNotifier("Advanced Combat Tracker").Show(toast);
37 | }
38 | else
39 | {
40 | throw new Exception("Unsupported OS");
41 | }
42 | }
43 |
44 | public static void SendLog(string log)
45 | {
46 | ActGlobals.oFormActMain.ParseRawLogLine(false, DateTime.Now, log);
47 | }
48 |
49 | public static void SendLog(BaseDTO dto, bool writeLog = true)
50 | {
51 | var log = Formatter.GetLog(dto);
52 | SendLog(log);
53 |
54 | if (writeLog)
55 | {
56 | Log.Info(LogType.LogLine, log);
57 | }
58 |
59 | if (Compat)
60 | {
61 | SendLog(Formatter.GetLog(dto, true));
62 | }
63 | }
64 |
65 | public static void SendTTS(string message)
66 | {
67 | if (!Config.TTS)
68 | {
69 | return;
70 | }
71 |
72 | ActGlobals.oFormActMain.TTS(message);
73 | }
74 |
75 | public static void SendToast(string message)
76 | {
77 | if (!Config.Toast)
78 | {
79 | return;
80 | }
81 |
82 | try
83 | {
84 | SendNativeToast(message);
85 | }
86 | catch
87 | {
88 | MessageBox.Show(message, Data.Title, MessageBoxButton.OK, MessageBoxImage.None, MessageBoxResult.OK, MessageBoxOptions.ServiceNotification);
89 | }
90 | }
91 |
92 | public static void SendWebhook(BaseDTO dto)
93 | {
94 | foreach (var item in Matcha.Config.Instance.Webhook)
95 | {
96 | if (item.Event != dto.EventType)
97 | {
98 | continue;
99 | }
100 |
101 | SendWebhook(dto, item);
102 | }
103 | }
104 |
105 | public static void SendWebhook(BaseDTO dto, ConfigWebhook webhook)
106 | {
107 | var eventName = Enum.GetName(typeof(EventType), dto.EventType);
108 |
109 | if (webhook.Type == RequestType.JSON)
110 | {
111 | var content = new JObject(
112 | new JProperty("event", eventName),
113 | new JProperty("data", JToken.FromObject(dto)));
114 | _ = Request.SendJson(webhook.Endpoint, webhook.Header, content.ToString());
115 | }
116 | else
117 | {
118 | _ = Request.Send(webhook.Endpoint, webhook.Type, webhook.Header, new Dictionary
119 | {
120 | { "event", eventName },
121 | { "data", dto.ToJSON() }
122 | });
123 | }
124 | }
125 |
126 | public static void Send(BaseDTO dto)
127 | {
128 | Send(Formatter.GetEventText(dto));
129 | }
130 |
131 | public static void Send(string message)
132 | {
133 | if (!string.IsNullOrEmpty(message))
134 | {
135 | SendTTS(message);
136 | SendToast(message);
137 | }
138 | }
139 | }
140 | }
141 |
--------------------------------------------------------------------------------
/utils/update-opcode.mjs:
--------------------------------------------------------------------------------
1 | import { readCsv } from './lib/csv.mjs'
2 | import { formatOpcode, parseOpcode } from './lib/opcode.mjs'
3 | import { join } from 'path'
4 | import { fileURLToPath } from 'url'
5 | import { writeFileSync } from 'fs'
6 |
7 | const __dirname = fileURLToPath(new URL('.', import.meta.url))
8 |
9 | const opcodes = [
10 | 'ActorControl',
11 | 'ActorControlSelf',
12 | {
13 | // https://github.com/quisquous/cactbot/blob/main/plugin/CactbotEventSource/FateWatcher.cs#L45
14 | key: 'CEDirector',
15 | // this is not the actual key, just a hack
16 | karashiiro: '_GH_CEDirector'
17 | },
18 | {
19 | key: 'CompanyAirshipStatus',
20 | karashiiro: 'AirshipTimers'
21 | },
22 | {
23 | key: 'CompanySubmersibleStatus',
24 | karashiiro: 'SubmarineTimers'
25 | },
26 | {
27 | key: 'ContentFinderNotifyPop',
28 | karashiiro: 'CFNotify'
29 | },
30 | {
31 | key: 'ResumeEventScene32'
32 | // karashiiro: 'MiniCactpotInit'
33 | },
34 | 'EventPlay',
35 | 'EventStart',
36 | 'Examine',
37 | 'FateInfo',
38 | 'InitZone',
39 | 'InventoryTransaction',
40 | 'ItemInfo',
41 | 'MarketBoardItemListing',
42 | 'MarketBoardItemListingCount',
43 | 'MarketBoardItemListingHistory',
44 | 'MarketBoardRequestItemListingInfo',
45 | 'NpcSpawn',
46 | 'PlayerSetup',
47 | 'PlayerSpawn',
48 | 'WorldVisitQueue'
49 | ]
50 |
51 | const outputOpcode = (key, value) =>
52 | `${' '.repeat(12)}{ 0x${formatOpcode(value)}, MatchaOpcode.${key} },`
53 |
54 | const outputKeys = () =>
55 | opcodes
56 | .map((item, index) => {
57 | if (typeof item === 'string') {
58 | item = { key: item }
59 | }
60 |
61 | return `${' '.repeat(8)}${item.key},`
62 | })
63 | .join('\n')
64 |
65 | const outputFromKarashiiro = (list, region) =>
66 | opcodes
67 | .map((item, index) => {
68 | if (typeof item === 'string') {
69 | item = { key: item }
70 | }
71 |
72 | const { key } = item
73 | if (item[region]) {
74 | return outputOpcode(key, item[region])
75 | }
76 |
77 | const fromKey = item.karashiiro || item.key
78 | const row = list.lists.ServerZoneIpcType.find(
79 | (row) => row.name === fromKey
80 | )
81 | const value = row ? row.opcode : 0xf000 + index
82 |
83 | return outputOpcode(key, value)
84 | })
85 | .join('\n')
86 |
87 | const outputFromWorker = (list) =>
88 | opcodes
89 | .map((item, index) => {
90 | if (typeof item === 'string') {
91 | item = { key: item }
92 | }
93 | const { key } = item
94 | const row = list.find(([rowKey]) => rowKey === key)
95 | const value = row ? row[1] : 0xf000 + index
96 |
97 | return outputOpcode(key, value)
98 | })
99 | .join('\n')
100 |
101 | ;(async () => {
102 | const karashiiroData = await fetch(
103 | 'https://raw.githubusercontent.com/karashiiro/FFXIVOpcodes/master/opcodes.json'
104 | )
105 | const parsedData = await karashiiroData.json()
106 |
107 | const globalOpcodes = parsedData.find((item) => item.region === 'Global')
108 |
109 | const cactbotFate = await fetch(
110 | 'https://raw.githubusercontent.com/quisquous/cactbot/main/plugin/CactbotEventSource/FateWatcher.cs'
111 | )
112 | const ceDirector =
113 | /cedirector_intl.+\n.+0x30.+\n\s+(0x[0-9a-fA-F]+),?\s*\n\s*\)/.exec(
114 | await cactbotFate.text()
115 | )
116 |
117 | if (ceDirector) {
118 | globalOpcodes.lists.ServerZoneIpcType.push({
119 | name: '_GH_CEDirector',
120 | opcode: parseOpcode(ceDirector[1])
121 | })
122 | }
123 |
124 | const workerData = await fetch(
125 | 'https://raw.githubusercontent.com/zhyupe/ffxiv-opcode-worker/master/cn-opcodes.csv'
126 | )
127 | const workerLines = readCsv(await workerData.text(), null, {
128 | header: 0,
129 | skip: 0
130 | })
131 | const cnOpcodes = workerLines.map(({ Name: name, Scope: scope, _ }) => {
132 | const valueColumn = _.reduce((val, content, index) => {
133 | return content ? index : val
134 | }, 0)
135 |
136 | const isClient = scope === 'ClientZoneIpc'
137 | return [name, (isClient ? 0x8000 : 0) + parseOpcode(_[valueColumn])]
138 | })
139 |
140 | writeFileSync(
141 | join(__dirname, '../Cafe.Matcha/Constant/MatchaOpcode.cs'),
142 | `// Copyright (c) FFCafe. All rights reserved.
143 | // Licensed under the AGPL-3.0 license. See LICENSE file in the project root for full license information.
144 |
145 | namespace Cafe.Matcha.Constant
146 | {
147 | using System.Collections.Generic;
148 |
149 | internal enum MatchaOpcode
150 | {
151 | ${outputKeys()}
152 | }
153 |
154 | internal static class OpcodeStorage
155 | {
156 | public static Dictionary Global = new Dictionary
157 | {
158 | ${outputFromKarashiiro(globalOpcodes, 'global')}
159 | };
160 | public static Dictionary China = new Dictionary
161 | {
162 | ${outputFromWorker(cnOpcodes, 'cn')}
163 | };
164 | }
165 | }
166 | `
167 | )
168 | })()
169 |
--------------------------------------------------------------------------------
/StyleCop.ruleset:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
--------------------------------------------------------------------------------
/Cafe.Matcha/Network/Structures/MarketTaxRates.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) FFCafe. All rights reserved.
2 | // Licensed under the AGPL-3.0 license. See LICENSE file in the project root for full license information.
3 |
4 | namespace Cafe.Matcha.Network.Structures
5 | {
6 | using System;
7 | using System.IO;
8 |
9 | ///
10 | /// This class represents the "Result Dialog" packet. This is also used e.g. for reduction results, but we only care about tax rates.
11 | /// We can do that by checking the "Category" field.
12 | ///
13 | public class MarketTaxRates : IMarketTaxRates
14 | {
15 | private MarketTaxRates()
16 | {
17 | }
18 |
19 | ///
20 | /// Gets the category of this ResultDialog packet.
21 | ///
22 | public uint Category { get; private set; }
23 |
24 | ///
25 | /// Gets the tax rate in Limsa Lominsa.
26 | ///
27 | public uint LimsaLominsaTax { get; private set; }
28 |
29 | ///
30 | /// Gets the tax rate in Gridania.
31 | ///
32 | public uint GridaniaTax { get; private set; }
33 |
34 | ///
35 | /// Gets the tax rate in Ul'dah.
36 | ///
37 | public uint UldahTax { get; private set; }
38 |
39 | ///
40 | /// Gets the tax rate in Ishgard.
41 | ///
42 | public uint IshgardTax { get; private set; }
43 |
44 | ///
45 | /// Gets the tax rate in Kugane.
46 | ///
47 | public uint KuganeTax { get; private set; }
48 |
49 | ///
50 | /// Gets the tax rate in the Crystarium.
51 | ///
52 | public uint CrystariumTax { get; private set; }
53 |
54 | ///
55 | /// Gets the tax rate in Sharlayan.
56 | ///
57 | public uint SharlayanTax { get; private set; }
58 |
59 | ///
60 | /// Gets the tax rate in Tuliyollal.
61 | ///
62 | public uint TuliyollalTax { get; private set; }
63 |
64 | ///
65 | /// Gets until when these values are valid.
66 | ///
67 | public DateTime ValidUntil { get; private set; }
68 |
69 | ///
70 | /// Read a object from memory.
71 | ///
72 | /// Data to read.
73 | /// A new object.
74 | public static MarketTaxRates Read(byte[] data)
75 | {
76 | using (var stream = new MemoryStream(data))
77 | {
78 | using (var reader = new BinaryReader(stream))
79 | {
80 | var output = new MarketTaxRates();
81 |
82 | output.Category = reader.ReadUInt32();
83 | stream.Position += 4;
84 | output.LimsaLominsaTax = reader.ReadUInt32();
85 | output.GridaniaTax = reader.ReadUInt32();
86 | output.UldahTax = reader.ReadUInt32();
87 | output.IshgardTax = reader.ReadUInt32();
88 | output.KuganeTax = reader.ReadUInt32();
89 | output.CrystariumTax = reader.ReadUInt32();
90 | output.SharlayanTax = reader.ReadUInt32();
91 | output.TuliyollalTax = reader.ReadUInt32();
92 |
93 | output.ValidUntil = DateTime.Now; // Dalamud never reads this packet, so setting it to Now just to be safe
94 |
95 | return output;
96 | }
97 | }
98 | }
99 |
100 | ///
101 | /// Generate a MarketTaxRates wrapper class from information located in a CustomTalk packet.
102 | ///
103 | /// Data to read.
104 | /// Returns a wrapped and ready-to-go MarketTaxRates record.
105 | public static MarketTaxRates ReadFromCustomTalk(byte[] data)
106 | {
107 | using (var stream = new MemoryStream(data))
108 | {
109 | using (var reader = new BinaryReader(stream))
110 | {
111 | return new MarketTaxRates
112 | {
113 | Category = 0xB0009, // shim
114 | LimsaLominsaTax = reader.ReadUInt32(),
115 | GridaniaTax = reader.ReadUInt32(),
116 | UldahTax = reader.ReadUInt32(),
117 | IshgardTax = reader.ReadUInt32(),
118 | KuganeTax = reader.ReadUInt32(),
119 | CrystariumTax = reader.ReadUInt32(),
120 | SharlayanTax = reader.ReadUInt32(),
121 | TuliyollalTax = reader.ReadUInt32(),
122 | ValidUntil = DateTimeOffset.FromUnixTimeSeconds(reader.ReadUInt32()).UtcDateTime,
123 | };
124 | }
125 | }
126 | }
127 | }
128 | }
129 |
--------------------------------------------------------------------------------
/Cafe.Matcha/Network/Structures/MarketBoardHistory.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) FFCafe. All rights reserved.
2 | // Licensed under the AGPL-3.0 license. See LICENSE file in the project root for full license information.
3 |
4 | namespace Cafe.Matcha.Network.Structures
5 | {
6 | using System;
7 | using System.Collections.Generic;
8 | using System.IO;
9 | using System.Text;
10 |
11 | ///
12 | /// This class represents the market board history from a game network packet.
13 | ///
14 | public class MarketBoardHistory : IMarketBoardHistory
15 | {
16 | ///
17 | /// Initializes a new instance of the class.
18 | ///
19 | internal MarketBoardHistory()
20 | {
21 | }
22 |
23 | ///
24 | /// Gets the catalog ID.
25 | ///
26 | public uint CatalogId { get; private set; }
27 |
28 | ///
29 | /// Gets the ID (for EXD) for the item being sold.
30 | ///
31 | public uint ItemId => this.CatalogId;
32 |
33 | ///
34 | /// Gets the list of individual item listings.
35 | ///
36 | public IReadOnlyList HistoryListings => this.InternalHistoryListings;
37 |
38 | ///
39 | /// Gets or sets a list of individual item listings.
40 | ///
41 | internal List InternalHistoryListings { get; set; } = new List();
42 |
43 | ///
44 | /// Read a object from memory.
45 | ///
46 | /// Data to read.
47 | /// A new object.
48 | public static MarketBoardHistory Read(byte[] data)
49 | {
50 | using (var stream = new MemoryStream(data))
51 | {
52 | using (var reader = new BinaryReader(stream))
53 | {
54 | var output = new MarketBoardHistory { CatalogId = reader.ReadUInt32() };
55 |
56 | var historyListings = new List();
57 | for (var i = 0; i < 20; i++)
58 | {
59 | var price = reader.ReadUInt32();
60 | if (price == 0)
61 | {
62 | // no price means we reached the end of available listings
63 | break;
64 | }
65 |
66 | var listingEntry = new MarketBoardHistoryListing
67 | {
68 | SalePrice = price,
69 | PurchaseTime = DateTimeOffset.FromUnixTimeSeconds(reader.ReadUInt32()).UtcDateTime,
70 | Quantity = reader.ReadUInt32(),
71 | IsHq = reader.ReadBoolean(),
72 | OnMannequin = reader.ReadBoolean(),
73 | BuyerName = Encoding.UTF8.GetString(reader.ReadBytes(0x20)).TrimEnd('\u0000'),
74 | };
75 |
76 | // Skip padding
77 | reader.ReadBytes(0x2);
78 |
79 | historyListings.Add(listingEntry);
80 | }
81 |
82 | output.InternalHistoryListings = historyListings;
83 |
84 | return output;
85 | }
86 | }
87 | }
88 |
89 | ///
90 | /// This class represents the market board history of a single item from the network packet.
91 | ///
92 | public class MarketBoardHistoryListing : IMarketBoardHistoryListing
93 | {
94 | ///
95 | /// Initializes a new instance of the class.
96 | ///
97 | internal MarketBoardHistoryListing()
98 | {
99 | }
100 |
101 | ///
102 | /// Gets the buyer's name.
103 | ///
104 | public string BuyerName { get; internal set; }
105 |
106 | ///
107 | /// Gets a value indicating whether the item is HQ.
108 | ///
109 | public bool IsHq { get; internal set; }
110 |
111 | ///
112 | /// Gets a value indicating whether the item is on a mannequin.
113 | ///
114 | public bool OnMannequin { get; internal set; }
115 |
116 | ///
117 | /// Gets the time of purchase.
118 | ///
119 | public DateTime PurchaseTime { get; internal set; }
120 |
121 | ///
122 | /// Gets the quantity.
123 | ///
124 | public uint Quantity { get; internal set; }
125 |
126 | ///
127 | /// Gets the sale price.
128 | ///
129 | public uint SalePrice { get; internal set; }
130 | }
131 | }
132 | }
133 |
--------------------------------------------------------------------------------
/Cafe.Matcha/Network/Handler/MarketBoardHandler.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) FFCafe. All rights reserved.
2 | // Licensed under the AGPL-3.0 license. See LICENSE file in the project root for full license information.
3 |
4 | namespace Cafe.Matcha.Network.Handler
5 | {
6 | using System;
7 | using System.Collections.Generic;
8 | using System.Threading;
9 | using Cafe.Matcha.Constant;
10 | using Cafe.Matcha.DTO;
11 | using Cafe.Matcha.Network.Structures;
12 |
13 | internal class MarketBoardHandler : AbstractHandler
14 | {
15 | ///
16 | /// ItemId sent in the last MarketBoard query.
17 | ///
18 | private uint queryItemId = 0;
19 |
20 | ///
21 | /// Is next incoming MarketBoardItemListing packet the first page.
22 | ///
23 | private bool isFirstPageIncoming = false;
24 |
25 | private Universalis.Client universalis;
26 |
27 | public MarketBoardHandler(Action fireEvent) : base(fireEvent)
28 | {
29 | universalis = new Universalis.Client(fireEvent);
30 |
31 | #if DEBUG
32 | universalis.UniversalisProcessor.Log += OnUniversalisLog;
33 | #endif
34 | }
35 |
36 | #if DEBUG
37 | private void OnUniversalisLog(object sender, string e)
38 | {
39 | Utils.Log.Info(LogType.Universalis, e);
40 | }
41 | #endif
42 |
43 | public override bool Handle(Packet packet)
44 | {
45 | universalis.Handle(packet);
46 |
47 | var opcode = packet.MatchaOpcode;
48 |
49 | // Required by Universalis but not us.
50 | if (opcode == MatchaOpcode.MarketBoardItemListingHistory)
51 | {
52 | return true;
53 | }
54 |
55 | // The client packet containing itemId. Deucalion Required.
56 | if (opcode == MatchaOpcode.MarketBoardRequestItemListingInfo)
57 | {
58 | if (packet.DataLength != 8)
59 | {
60 | return false;
61 | }
62 |
63 | var itemId = packet.ReadUInt32(0);
64 | if (itemId != 0)
65 | {
66 | InitMarketBoardListing(itemId);
67 | queryItemId = itemId;
68 | }
69 |
70 | return true;
71 | }
72 |
73 | // Indicates a new group of MarketBoardItemListing
74 | if (opcode == MatchaOpcode.MarketBoardItemListingCount)
75 | {
76 | isFirstPageIncoming = true;
77 | return true;
78 | }
79 |
80 | // Market listing response
81 | if (opcode == MatchaOpcode.MarketBoardItemListing)
82 | {
83 | var result = MarketBoardCurrentOfferings.Read(packet.GetRawData());
84 | var items = new List();
85 |
86 | uint itemId = 0;
87 | foreach (var item in result.ItemListings)
88 | {
89 | if (item.PricePerUnit == 0)
90 | {
91 | break;
92 | }
93 |
94 | itemId = item.ItemId;
95 | items.Add(new MarketBoardItemListingItem()
96 | {
97 | // Price = (int)(pricePerUnit * 1.05),
98 | Price = (int)item.PricePerUnit,
99 | Quantity = (int)item.ItemQuantity,
100 | HQ = item.IsHq
101 | });
102 | }
103 |
104 | if (itemId == 0)
105 | {
106 | return true;
107 | }
108 |
109 | // Call `MarketBoardRequestItemListingInfo` only when
110 | // 1.`queryItemId == 0`, which means client packet `MarketBoardRequestItemListingInfo` is not correctly handled.
111 | // 2. This is the first listing page (otherwise it have already been called)
112 |
113 | if (isFirstPageIncoming && queryItemId == 0)
114 | {
115 | InitMarketBoardListing(itemId);
116 | }
117 |
118 | fireEvent(new MarketBoardItemListingDTO()
119 | {
120 | Item = (int)itemId,
121 | Data = items,
122 | World = State.Instance.WorldId
123 | });
124 |
125 | // Always reset isFirstPageIncoming and queryItemId
126 | isFirstPageIncoming = false;
127 | queryItemId = 0;
128 |
129 | return true;
130 | }
131 |
132 | return false;
133 | }
134 |
135 | ///
136 | /// Init (reset overlay status) for specified itemId.
137 | ///
138 | /// ItemId
139 | private void InitMarketBoardListing(uint itemId)
140 | {
141 | fireEvent(new MarketBoardItemListingCountDTO()
142 | {
143 | Item = (int)itemId,
144 | World = State.Instance.WorldId
145 | });
146 | ThreadPool.QueueUserWorkItem(o => universalis.QueryItem(State.Instance.WorldId, itemId));
147 | queryItemId = itemId;
148 | }
149 | }
150 | }
151 |
--------------------------------------------------------------------------------
/Cafe.Matcha/data/type.json:
--------------------------------------------------------------------------------
1 | {
2 | "1": {
3 | "chs": "随机任务",
4 | "en": "Duty Roulette",
5 | "ja": "コンテンツルーレット",
6 | "de": "Zufallsinhalt",
7 | "fr": "Missions aléatoires"
8 | },
9 | "2": {
10 | "chs": "迷宫挑战",
11 | "en": "Dungeons",
12 | "ja": "ダンジョン",
13 | "de": "Dungeons",
14 | "fr": "Donjons"
15 | },
16 | "3": {
17 | "chs": "行会令",
18 | "en": "Guildhests",
19 | "ja": "ギルドオーダー",
20 | "de": "Gildengeheiße",
21 | "fr": "Opérations de guilde"
22 | },
23 | "4": {
24 | "chs": "讨伐歼灭战",
25 | "en": "Trials",
26 | "ja": "討伐・討滅戦",
27 | "de": "Prüfungen",
28 | "fr": "Défis"
29 | },
30 | "5": {
31 | "chs": "大型任务",
32 | "en": "Raids",
33 | "ja": "レイド",
34 | "de": "Raids",
35 | "fr": "Raids"
36 | },
37 | "6": {
38 | "chs": "对战",
39 | "en": "PvP",
40 | "ja": "PvP",
41 | "de": "PvP",
42 | "fr": "JcJ"
43 | },
44 | "7": {
45 | "chs": "任务战斗",
46 | "en": "Quest Battles",
47 | "ja": "クエストバトル",
48 | "de": "Auftragskämpfe",
49 | "fr": "Batailles de quête"
50 | },
51 | "8": {
52 | "chs": "危命任务",
53 | "en": "FATEs",
54 | "ja": "F.A.T.E.",
55 | "de": "FATEs",
56 | "fr": "ALÉA"
57 | },
58 | "9": {
59 | "chs": "寻宝",
60 | "en": "Treasure Hunt",
61 | "ja": "トレジャーハント",
62 | "de": "Schatzsuche",
63 | "fr": "Chasse aux trésors"
64 | },
65 | "10": {
66 | "chs": "理符任务",
67 | "en": "Levequests",
68 | "ja": "リーヴ",
69 | "de": "Freibriefe",
70 | "fr": "Mandats"
71 | },
72 | "11": {
73 | "chs": "大国防联军",
74 | "en": "Grand Company",
75 | "ja": "グランドカンパニー",
76 | "de": "Staatliche Gesellschaft",
77 | "fr": "Grandes compagnies"
78 | },
79 | "12": {
80 | "chs": "搭档",
81 | "en": "Companions",
82 | "ja": "バディ",
83 | "de": "Mitstreiter",
84 | "fr": "Compagnon"
85 | },
86 | "13": {
87 | "chs": "友好部族任务",
88 | "en": "Society Quests",
89 | "ja": "友好部族クエスト",
90 | "de": "Freundesvölker",
91 | "fr": "Quêtes des peuples alliés"
92 | },
93 | "14": {
94 | "chs": "完成情况",
95 | "en": "Overall Completion",
96 | "ja": "達成数ボーナス",
97 | "de": "Agenda-Abschlusszahl",
98 | "fr": "Bonus de réussites"
99 | },
100 | "15": {
101 | "chs": "最优队员",
102 | "en": "Player Commendation",
103 | "ja": "MIP",
104 | "de": "Wertvollster Spieler",
105 | "fr": "Honneurs"
106 | },
107 | "16": {
108 | "chs": "大地使者",
109 | "en": "Disciples of the Land",
110 | "ja": "ギャザラー",
111 | "de": "Sammeln",
112 | "fr": "Récolte"
113 | },
114 | "17": {
115 | "chs": "能工巧匠",
116 | "en": "Disciples of the Hand",
117 | "ja": "クラフター",
118 | "de": "Synthese",
119 | "fr": "Artisanat"
120 | },
121 | "18": {
122 | "chs": "雇员",
123 | "en": "Retainer Ventures",
124 | "ja": "リテイナー",
125 | "de": "Unternehmungen",
126 | "fr": "Servants"
127 | },
128 | "19": {
129 | "chs": "金碟游乐场",
130 | "en": "Gold Saucer",
131 | "ja": "ゴールドソーサー",
132 | "de": "Gold Saucer",
133 | "fr": "Gold Saucer"
134 | },
135 | "21": {
136 | "chs": "深层迷宫",
137 | "en": "Deep Dungeons",
138 | "ja": "ディープダンジョン",
139 | "de": "Tiefe Gewölbe",
140 | "fr": "Donjons sans fond"
141 | },
142 | "24": {
143 | "chs": "天书奇谈",
144 | "en": "Wondrous Tails",
145 | "ja": "クロの空想帳",
146 | "de": "Abenteueralbum",
147 | "fr": "Aventures imaginaires"
148 | },
149 | "25": {
150 | "chs": "老主顾交易",
151 | "en": "Custom Deliveries",
152 | "ja": "お得意様取引",
153 | "de": "Wunschlieferungen",
154 | "fr": "Commandes spéciales"
155 | },
156 | "26": {
157 | "chs": "禁地优雷卡",
158 | "en": "Eureka",
159 | "ja": "禁断の地エウレカ",
160 | "de": "Eureka",
161 | "fr": "Eurêka"
162 | },
163 | "27": {
164 | "chs": "假面狂欢",
165 | "en": "The Masked Carnivale",
166 | "ja": "マスクカーニバル",
167 | "de": "Große Maskerade",
168 | "fr": "La Grande Mascarade"
169 | },
170 | "28": {
171 | "chs": "绝境战",
172 | "en": "Ultimate Raids",
173 | "ja": "絶レイド",
174 | "de": "Fatale Raids",
175 | "fr": "Raids fatals"
176 | },
177 | "29": {
178 | "chs": "天佑女王",
179 | "en": "Save the Queen",
180 | "ja": "セイブ・ザ・クイーン",
181 | "de": "Königinnenwache",
182 | "fr": "Garde-la-Reine"
183 | },
184 | "30": {
185 | "chs": "特殊迷宫探索",
186 | "en": "V&C Dungeon Finder",
187 | "ja": "特殊ダンジョン探索",
188 | "de": "Gewölbesuche",
189 | "fr": "Donjons spéciaux"
190 | },
191 | "31": {
192 | "chs": "出海垂钓",
193 | "en": "Ocean Fishing",
194 | "ja": "オーシャンフィッシング",
195 | "de": "Auf großer Fahrt",
196 | "fr": "Pêche en mer"
197 | },
198 | "32": {
199 | "chs": "九宫幻卡",
200 | "en": "Triple Triad",
201 | "ja": "トリプルトライアド",
202 | "de": "Triple Triad",
203 | "fr": "Triple Triade"
204 | },
205 | "33": {
206 | "chs": "怪物狩猎",
207 | "en": "The Hunt",
208 | "ja": "モブハント",
209 | "de": "Hohe Jagd",
210 | "fr": "Contrats de chasse"
211 | },
212 | "34": {
213 | "chs": "钓鱼",
214 | "en": "Fishing",
215 | "ja": "釣り",
216 | "de": "Fischen",
217 | "fr": "Pêche"
218 | },
219 | "35": {
220 | "chs": "机遇任务",
221 | "en": "GATE",
222 | "ja": "G.A.T.E.",
223 | "de": "GATE",
224 | "fr": "JACTA"
225 | },
226 | "36": {
227 | "chs": "开拓无人岛",
228 | "en": "Island Sanctuary",
229 | "ja": "無人島開拓",
230 | "de": "Inselparadies",
231 | "fr": "Félicité insulaire"
232 | },
233 | "37": {
234 | "chs": "诛灭战",
235 | "en": "Chaotic Alliance Raid",
236 | "ja": "滅アライアンスレイド",
237 | "de": "Chaotische Allianz-Raids",
238 | "fr": "Raids chaotiques"
239 | },
240 | "38": {
241 | "chs": "蜃景幻界新月岛",
242 | "en": "Occult Crescent",
243 | "ja": "蜃気楼の島 クレセントアイル",
244 | "de": "Kreszentia",
245 | "fr": "Île de Lunule"
246 | }
247 | }
--------------------------------------------------------------------------------
/Cafe.Matcha/Network/Universalis/Api.cs:
--------------------------------------------------------------------------------
1 | namespace Cafe.Matcha.Network.Universalis
2 | {
3 | using System;
4 | using System.Collections.Generic;
5 | using System.Linq;
6 | using System.Threading.Tasks;
7 | using Cafe.Matcha.Network.Structures;
8 | using Cafe.Matcha.Utils;
9 | using Newtonsoft.Json;
10 |
11 | internal class Api
12 | {
13 | private const string ApiBase = "https://universalis.app";
14 |
15 | private readonly PacketProcessor _packetProcessor;
16 | private readonly string _apiKey;
17 |
18 | public Api(PacketProcessor packetProcessor, string apiKey)
19 | {
20 | _packetProcessor = packetProcessor;
21 | _apiKey = apiKey;
22 | }
23 |
24 | public async void Upload(uint worldId, MarketBoardItemRequest request)
25 | {
26 | _packetProcessor.Log?.Invoke(this, "Starting Universalis upload.");
27 | var uploader = _packetProcessor.LocalContentId;
28 |
29 | var uploadObject = new UniversalisItemUploadRequest
30 | {
31 | WorldId = worldId,
32 | UploaderId = uploader.ToString(),
33 | ItemId = request.Listings.FirstOrDefault()?.ItemId ?? 0,
34 | Listings = new List(),
35 | Sales = new List(),
36 | };
37 |
38 | foreach (var marketBoardItemListing in request.Listings)
39 | {
40 | #pragma warning disable CS0618 // Type or member is obsolete
41 | var universalisListing = new UniversalisItemListingsEntry
42 | {
43 | ListingId = marketBoardItemListing.ListingId.ToString(),
44 | Hq = marketBoardItemListing.IsHq,
45 | SellerId = marketBoardItemListing.RetainerOwnerId.ToString(),
46 | RetainerName = marketBoardItemListing.RetainerName,
47 | RetainerId = marketBoardItemListing.RetainerId.ToString(),
48 | CreatorId = marketBoardItemListing.ArtisanId.ToString(),
49 | CreatorName = marketBoardItemListing.PlayerName,
50 | OnMannequin = marketBoardItemListing.OnMannequin,
51 | LastReviewTime = ((DateTimeOffset)marketBoardItemListing.LastReviewTime).ToUnixTimeSeconds(),
52 | PricePerUnit = marketBoardItemListing.PricePerUnit,
53 | Quantity = marketBoardItemListing.ItemQuantity,
54 | RetainerCity = marketBoardItemListing.RetainerCityId,
55 | Materia = new List(),
56 | };
57 | #pragma warning restore CS0618 // Type or member is obsolete
58 |
59 | foreach (var itemMateria in marketBoardItemListing.Materia)
60 | {
61 | universalisListing.Materia.Add(new UniversalisItemMateria
62 | {
63 | MateriaId = itemMateria.MateriaId,
64 | SlotId = itemMateria.Index,
65 | });
66 | }
67 |
68 | uploadObject.Listings.Add(universalisListing);
69 | }
70 |
71 | foreach (var marketBoardHistoryListing in request.History)
72 | {
73 | uploadObject.Sales.Add(new UniversalisHistoryEntry
74 | {
75 | BuyerName = marketBoardHistoryListing.BuyerName,
76 | Hq = marketBoardHistoryListing.IsHq,
77 | OnMannequin = marketBoardHistoryListing.OnMannequin,
78 | PricePerUnit = marketBoardHistoryListing.SalePrice,
79 | Quantity = marketBoardHistoryListing.Quantity,
80 | Timestamp = ((DateTimeOffset)marketBoardHistoryListing.PurchaseTime).ToUnixTimeSeconds(),
81 | });
82 | }
83 |
84 | var uploadPath = "/upload";
85 | await Request.SendAsJson($"{ApiBase}{uploadPath}/{_apiKey}", "", uploadObject);
86 |
87 | _packetProcessor.Log?.Invoke(this, $"Universalis data upload for item#{request.Listings.FirstOrDefault()?.CatalogId ?? 0} completed");
88 | }
89 |
90 | public static async Task>> ListByDC(ushort worldId, uint itemId)
91 | {
92 | var hasWorld = Data.Instance.Worlds.TryGetValue(worldId, out var world);
93 | if (!hasWorld)
94 | {
95 | return null;
96 | }
97 |
98 | var url = $"{ApiBase}/api/{world.EnglishDataCenter}/{itemId}";
99 | var res = await Request.Send(url);
100 |
101 | try
102 | {
103 | var content = await res.Content.ReadAsStringAsync();
104 | var data = JsonConvert.DeserializeObject(content);
105 | var ret = new Dictionary>();
106 |
107 | foreach (var item in data.ListingItems)
108 | {
109 | var itemWorld = Data.Instance.Worlds
110 | .FirstOrDefault(row => item.WorldName == row.Value.LocalName || item.WorldName == row.Value.EnglishName);
111 |
112 | if (itemWorld.Key == 0)
113 | {
114 | continue;
115 | }
116 |
117 | var hasWorldResult = ret.TryGetValue(itemWorld.Key, out var worldList);
118 | if (!hasWorldResult)
119 | {
120 | worldList = new List();
121 | ret.Add(itemWorld.Key, worldList);
122 | }
123 |
124 | worldList.Add(item);
125 | }
126 |
127 | return ret;
128 | }
129 | catch (Exception e)
130 | {
131 | Log.Error(Constant.LogType.Universalis, $"获取物价信息失败 {e.Message}");
132 | #if DEBUG
133 | Log.Debug(Constant.LogType.Universalis, e.StackTrace);
134 | #endif
135 | }
136 |
137 | return null;
138 | }
139 | }
140 | }
141 |
--------------------------------------------------------------------------------
/Cafe.Matcha/Network/State.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) FFCafe. All rights reserved.
2 | // Licensed under the AGPL-3.0 license. See LICENSE file in the project root for full license information.
3 |
4 | namespace Cafe.Matcha.Network
5 | {
6 | using System;
7 | using System.Collections.Generic;
8 | using System.Numerics;
9 | using Cafe.Matcha.Telemetry;
10 | using Cafe.Matcha.Utils;
11 |
12 | internal class FateState
13 | {
14 | public uint StartTime;
15 | public uint Duration;
16 | public int Progress;
17 | }
18 |
19 | internal class NpcState
20 | {
21 | public uint BNpcName;
22 | public uint Location;
23 | public uint Fate;
24 | public ushort Level;
25 | public uint CurHP;
26 | public uint MaxHP;
27 | public Vector3 Position;
28 | }
29 |
30 | internal class State : StaticBindingTarget
31 | {
32 | private ushort worldId = 0;
33 | private ushort zoneId = 0;
34 |
35 | public long LastZoneChange { get; private set; } = 0;
36 |
37 | public ushort InstanceId { get; private set; } = 0;
38 |
39 | public ushort ServerId { get; private set; } = 0;
40 |
41 | public ushort ContentId { get; private set; } = 0;
42 |
43 | public uint FishingBait { get; set; } = 0;
44 |
45 | public readonly StateManager Fate = new StateManager();
46 | public readonly StateManager Npc = new StateManager();
47 |
48 | private Fate fateTelemetry = new Fate();
49 | private Npc npcTelemetry = new Npc();
50 |
51 | public ushort WorldId
52 | {
53 | get
54 | {
55 | if (worldId == 0 && ParsePlugin.Instance != null)
56 | {
57 | return (ushort)ParsePlugin.Instance.GetServer();
58 | }
59 |
60 | return worldId;
61 | }
62 |
63 | private set
64 | {
65 | worldId = value;
66 | }
67 | }
68 |
69 | public ushort ZoneId
70 | {
71 | get
72 | {
73 | if (zoneId == 0 && ParsePlugin.Instance != null)
74 | {
75 | return (ushort)ParsePlugin.Instance.GetCurrentTerritoryID();
76 | }
77 |
78 | return zoneId;
79 | }
80 | private set
81 | {
82 | zoneId = value;
83 | }
84 | }
85 |
86 | public State()
87 | {
88 | Fate.OnChanged += Fate_OnChanged;
89 | Fate.OnRemoved += Fate_OnRemoved;
90 | Npc.OnChanged += Npc_OnChanged;
91 | Npc.OnRemoved += Npc_OnRemoved;
92 | }
93 |
94 | private void Fate_OnChanged(uint id, FateState state)
95 | {
96 | fateTelemetry.Send(id, state);
97 | }
98 |
99 | private void Fate_OnRemoved(uint id, FateState state)
100 | {
101 | // Emit only on progress=100 or expired
102 | if (state.Progress == 100 || IsFateNearEnd(state))
103 | {
104 | state.Progress = -1;
105 | fateTelemetry.Send(id, state);
106 | }
107 | }
108 |
109 | private bool IsFateNearEnd(FateState state)
110 | {
111 | if (state.StartTime == 0 || state.Duration == 0)
112 | {
113 | return false;
114 | }
115 |
116 | return Helper.Now / 1000 + 30 > state.StartTime + state.Duration;
117 | }
118 |
119 | private void Npc_OnChanged(uint id, NpcState state)
120 | {
121 | npcTelemetry.Send(id, state);
122 | }
123 |
124 | private void Npc_OnRemoved(uint id, NpcState state)
125 | {
126 | state.CurHP = 0;
127 | npcTelemetry.Send(id, state);
128 | }
129 |
130 | public void HandleInitZone(ushort serverId, ushort zoneId, ushort instanceId, ushort contentId)
131 | {
132 | ServerId = serverId;
133 | ZoneId = zoneId;
134 | InstanceId = instanceId;
135 | ContentId = contentId;
136 |
137 | LastZoneChange = DateTimeOffset.Now.ToUnixTimeMilliseconds();
138 | Fate.Clear();
139 | Npc.Clear();
140 |
141 | Log.Info(Constant.LogType.State, $"InitZone: server={serverId}, zone={zoneId}, instance={instanceId}, time={LastZoneChange}");
142 | }
143 |
144 | public void HandleWorldId(ushort worldId, bool isCurrentPlayer)
145 | {
146 | if ((ContentId == 0 || isCurrentPlayer) && this.worldId != worldId)
147 | {
148 | Log.Info(Constant.LogType.State, $"WorldId: {worldId}");
149 | WorldId = worldId;
150 | }
151 | }
152 |
153 | internal class StateManager where T : new()
154 | {
155 | private readonly Dictionary store = new Dictionary();
156 | private readonly object storeLock = new object();
157 |
158 | public void Update(uint id, Func action)
159 | {
160 | bool isCreate = false;
161 | T state;
162 | lock (storeLock)
163 | {
164 | if (!store.TryGetValue(id, out state))
165 | {
166 | isCreate = true;
167 | state = new T();
168 | store.Add(id, state);
169 | }
170 | }
171 |
172 | if (action(state) || isCreate)
173 | {
174 | #if DEBUG
175 | Log.Debug(Constant.LogType.State, $"<{typeof(T).Name}> Emitting OnChanged for Id={id}, isCreate={isCreate}, {state}");
176 | #endif
177 | OnChanged?.Invoke(id, state);
178 | }
179 | }
180 |
181 | public T this[uint id]
182 | {
183 | get { return store[id]; }
184 | }
185 |
186 | public bool Contains(uint id)
187 | {
188 | return store.ContainsKey(id);
189 | }
190 |
191 | public void Remove(uint id)
192 | {
193 | if (store.TryGetValue(id, out var state))
194 | {
195 | store.Remove(id);
196 | OnRemoved?.Invoke(id, state);
197 | }
198 | }
199 |
200 | public void Clear()
201 | {
202 | store.Clear();
203 | }
204 |
205 | public delegate void EventHandler(uint id, T state);
206 | public event EventHandler OnChanged;
207 | public event EventHandler OnRemoved;
208 | }
209 | }
210 | }
211 |
--------------------------------------------------------------------------------
/Cafe.Matcha/Utils/Request.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) FFCafe. All rights reserved.
2 | // Licensed under the AGPL-3.0 license. See LICENSE file in the project root for full license information.
3 |
4 | ///
5 | ///
6 | ///
7 | namespace Cafe.Matcha.Utils
8 | {
9 | using System;
10 | using System.Collections.Generic;
11 | using System.Net.Http;
12 | using System.Text;
13 | using System.Threading.Tasks;
14 | using Cafe.Matcha.Constant;
15 | using Newtonsoft.Json;
16 |
17 | internal class RequestHeaders : List> { }
18 |
19 | internal class Request
20 | {
21 | private static RequestHeaders ParseHeader(string header)
22 | {
23 | if (string.IsNullOrEmpty(header))
24 | {
25 | return null;
26 | }
27 |
28 | var headers = new RequestHeaders();
29 | foreach (string line in header.Split('\n'))
30 | {
31 | var pos = line.IndexOf(':');
32 | if (pos == -1)
33 | {
34 | continue;
35 | }
36 |
37 | headers.Add(new KeyValuePair(line.Substring(0, pos).Trim(), line.Substring(pos + 1).Trim()));
38 | }
39 |
40 | return headers;
41 | }
42 |
43 | public static async Task SendJson(string endpoint, string header, string json)
44 | {
45 | return await SendJson(endpoint, ParseHeader(header), json);
46 | }
47 |
48 | public static async Task SendJson(string endpoint, RequestHeaders header, string json)
49 | {
50 | var content = new StringContent(json, Encoding.UTF8, "application/json");
51 | return await Send(endpoint, HttpMethod.Post, header, content);
52 | }
53 |
54 | public static async Task SendAsJson(string endpoint, string header, object data)
55 | {
56 | return await SendAsJson(endpoint, ParseHeader(header), data);
57 | }
58 |
59 | public static async Task SendAsJson(string endpoint, RequestHeaders header, object data)
60 | {
61 | string json = JsonConvert.SerializeObject(data);
62 | return await SendJson(endpoint, header, json);
63 | }
64 |
65 | public static async Task SendAsMultipart(string endpoint, RequestHeaders header, MultipartFormDataContent data)
66 | {
67 | return await Send(endpoint, HttpMethod.Post, header, data);
68 | }
69 |
70 | public static async Task Send(string endpoint)
71 | {
72 | return await Send(endpoint, HttpMethod.Get, null, null);
73 | }
74 |
75 | public static async Task Send(string endpoint, RequestType type, Dictionary data)
76 | {
77 | return await Send(endpoint, type, (RequestHeaders)null, data);
78 | }
79 |
80 | public static async Task Send(string endpoint, RequestType type, string header, Dictionary data)
81 | {
82 | return await Send(endpoint, type, ParseHeader(header), data);
83 | }
84 |
85 | public static async Task Send(string endpoint, RequestType type, RequestHeaders header, Dictionary data)
86 | {
87 | switch (type)
88 | {
89 | case RequestType.Get:
90 | StringBuilder sb = new StringBuilder(endpoint);
91 |
92 | bool hasQuery = endpoint.Contains("?");
93 | foreach (var pair in data)
94 | {
95 | sb.Append(hasQuery ? '&' : '?');
96 | sb.Append(Uri.EscapeDataString(pair.Key));
97 | sb.Append('=');
98 | sb.Append(Uri.EscapeDataString(pair.Value));
99 |
100 | hasQuery = true;
101 | }
102 |
103 | return await Send(sb.ToString(), HttpMethod.Get, header, null);
104 | case RequestType.JSON:
105 | return await SendAsJson(endpoint, header, data);
106 | case RequestType.Form:
107 | return await Send(endpoint, HttpMethod.Post, header, new FormUrlEncodedContent(data));
108 | case RequestType.Multipart:
109 | MultipartFormDataContent multipartData = new MultipartFormDataContent();
110 | foreach (var pair in data)
111 | {
112 | multipartData.Add(new StringContent(pair.Value), pair.Key);
113 | }
114 |
115 | return await Send(endpoint, HttpMethod.Post, header, multipartData);
116 | }
117 |
118 | return null;
119 | }
120 |
121 | public static async Task Send(string endpoint, HttpMethod method, RequestHeaders header, HttpContent data)
122 | {
123 | #if DEBUG
124 | Log.Debug(LogType.Request, $"{method} {endpoint}");
125 | #endif
126 | try
127 | {
128 | var uri = new Uri(endpoint);
129 | using (HttpClient client = new HttpClient())
130 | {
131 | var request = new HttpRequestMessage()
132 | {
133 | RequestUri = uri,
134 | Method = method
135 | };
136 |
137 | request.Headers.Add("User-Agent", $"Cafe.Matcha/{Data.Version}");
138 | if (header != null)
139 | {
140 | foreach (var pair in header)
141 | {
142 | request.Headers.Add(pair.Key, pair.Value);
143 | }
144 | }
145 |
146 | if (data != null)
147 | {
148 | request.Content = data;
149 | #if DEBUG
150 | var body = await data.ReadAsStringAsync();
151 | Log.Debug(LogType.Request, $"[Content] {body}");
152 | #endif
153 | }
154 |
155 | var res = await client.SendAsync(request);
156 | #if DEBUG
157 | var content = await res.Content.ReadAsStringAsync();
158 | Log.Debug(LogType.Request, $"[Response] {res.StatusCode} {content}");
159 | #endif
160 |
161 | return res;
162 | }
163 | }
164 | catch (Exception e)
165 | {
166 | #if DEBUG
167 | Log.Error(LogType.Request, $"{e.Message}");
168 | #endif
169 | return null;
170 | }
171 | }
172 | }
173 | }
174 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 |
2 | # Created by https://www.gitignore.io/api/csharp,visualstudio
3 | # Edit at https://www.gitignore.io/?templates=csharp,visualstudio
4 |
5 | ### Csharp ###
6 | ## Ignore Visual Studio temporary files, build results, and
7 | ## files generated by popular Visual Studio add-ons.
8 | ##
9 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
10 |
11 | # User-specific files
12 | *.rsuser
13 | *.suo
14 | *.user
15 | *.userosscache
16 | *.sln.docstates
17 |
18 | # User-specific files (MonoDevelop/Xamarin Studio)
19 | *.userprefs
20 |
21 | # Mono auto generated files
22 | mono_crash.*
23 |
24 | # Build results
25 | [Dd]ebug/
26 | [Dd]ebugPublic/
27 | [Rr]elease/
28 | [Rr]eleases/
29 | x64/
30 | x86/
31 | [Aa][Rr][Mm]/
32 | [Aa][Rr][Mm]64/
33 | bld/
34 | [Bb]in/
35 | [Oo]bj/
36 | [Ll]og/
37 |
38 | # Visual Studio 2015/2017 cache/options directory
39 | .vs/
40 | # Uncomment if you have tasks that create the project's static files in wwwroot
41 | #wwwroot/
42 |
43 | # Visual Studio 2017 auto generated files
44 | Generated\ Files/
45 |
46 | # MSTest test Results
47 | [Tt]est[Rr]esult*/
48 | [Bb]uild[Ll]og.*
49 |
50 | # NUnit
51 | *.VisualState.xml
52 | TestResult.xml
53 | nunit-*.xml
54 |
55 | # Build Results of an ATL Project
56 | [Dd]ebugPS/
57 | [Rr]eleasePS/
58 | dlldata.c
59 |
60 | # Benchmark Results
61 | BenchmarkDotNet.Artifacts/
62 |
63 | # .NET Core
64 | project.lock.json
65 | project.fragment.lock.json
66 | artifacts/
67 |
68 | # StyleCop
69 | StyleCopReport.xml
70 |
71 | # Files built by Visual Studio
72 | *_i.c
73 | *_p.c
74 | *_h.h
75 | *.ilk
76 | *.meta
77 | *.obj
78 | *.iobj
79 | *.pch
80 | *.pdb
81 | *.ipdb
82 | *.pgc
83 | *.pgd
84 | *.rsp
85 | *.sbr
86 | *.tlb
87 | *.tli
88 | *.tlh
89 | *.tmp
90 | *.tmp_proj
91 | *_wpftmp.csproj
92 | *.log
93 | *.vspscc
94 | *.vssscc
95 | .builds
96 | *.pidb
97 | *.svclog
98 | *.scc
99 |
100 | # Chutzpah Test files
101 | _Chutzpah*
102 |
103 | # Visual C++ cache files
104 | ipch/
105 | *.aps
106 | *.ncb
107 | *.opendb
108 | *.opensdf
109 | *.sdf
110 | *.cachefile
111 | *.VC.db
112 | *.VC.VC.opendb
113 |
114 | # Visual Studio profiler
115 | *.psess
116 | *.vsp
117 | *.vspx
118 | *.sap
119 |
120 | # Visual Studio Trace Files
121 | *.e2e
122 |
123 | # TFS 2012 Local Workspace
124 | $tf/
125 |
126 | # Guidance Automation Toolkit
127 | *.gpState
128 |
129 | # ReSharper is a .NET coding add-in
130 | _ReSharper*/
131 | *.[Rr]e[Ss]harper
132 | *.DotSettings.user
133 |
134 | # JustCode is a .NET coding add-in
135 | .JustCode
136 |
137 | # TeamCity is a build add-in
138 | _TeamCity*
139 |
140 | # DotCover is a Code Coverage Tool
141 | *.dotCover
142 |
143 | # AxoCover is a Code Coverage Tool
144 | .axoCover/*
145 | !.axoCover/settings.json
146 |
147 | # Visual Studio code coverage results
148 | *.coverage
149 | *.coveragexml
150 |
151 | # NCrunch
152 | _NCrunch_*
153 | .*crunch*.local.xml
154 | nCrunchTemp_*
155 |
156 | # MightyMoose
157 | *.mm.*
158 | AutoTest.Net/
159 |
160 | # Web workbench (sass)
161 | .sass-cache/
162 |
163 | # Installshield output folder
164 | [Ee]xpress/
165 |
166 | # DocProject is a documentation generator add-in
167 | DocProject/buildhelp/
168 | DocProject/Help/*.HxT
169 | DocProject/Help/*.HxC
170 | DocProject/Help/*.hhc
171 | DocProject/Help/*.hhk
172 | DocProject/Help/*.hhp
173 | DocProject/Help/Html2
174 | DocProject/Help/html
175 |
176 | # Click-Once directory
177 | publish/
178 |
179 | # Publish Web Output
180 | *.[Pp]ublish.xml
181 | *.azurePubxml
182 | # Note: Comment the next line if you want to checkin your web deploy settings,
183 | # but database connection strings (with potential passwords) will be unencrypted
184 | *.pubxml
185 | *.publishproj
186 |
187 | # Microsoft Azure Web App publish settings. Comment the next line if you want to
188 | # checkin your Azure Web App publish settings, but sensitive information contained
189 | # in these scripts will be unencrypted
190 | PublishScripts/
191 |
192 | # NuGet Packages
193 | *.nupkg
194 | # NuGet Symbol Packages
195 | *.snupkg
196 | # The packages folder can be ignored because of Package Restore
197 | **/[Pp]ackages/*
198 | # except build/, which is used as an MSBuild target.
199 | !**/[Pp]ackages/build/
200 | # Uncomment if necessary however generally it will be regenerated when needed
201 | #!**/[Pp]ackages/repositories.config
202 | # NuGet v3's project.json files produces more ignorable files
203 | *.nuget.props
204 | *.nuget.targets
205 |
206 | # Microsoft Azure Build Output
207 | csx/
208 | *.build.csdef
209 |
210 | # Microsoft Azure Emulator
211 | ecf/
212 | rcf/
213 |
214 | # Windows Store app package directories and files
215 | AppPackages/
216 | BundleArtifacts/
217 | Package.StoreAssociation.xml
218 | _pkginfo.txt
219 | *.appx
220 | *.appxbundle
221 | *.appxupload
222 |
223 | # Visual Studio cache files
224 | # files ending in .cache can be ignored
225 | *.[Cc]ache
226 | # but keep track of directories ending in .cache
227 | !?*.[Cc]ache/
228 |
229 | # Others
230 | ClientBin/
231 | ~$*
232 | *~
233 | *.dbmdl
234 | *.dbproj.schemaview
235 | *.jfm
236 | *.pfx
237 | *.publishsettings
238 | orleans.codegen.cs
239 |
240 | # Including strong name files can present a security risk
241 | # (https://github.com/github/gitignore/pull/2483#issue-259490424)
242 | #*.snk
243 |
244 | # Since there are multiple workflows, uncomment next line to ignore bower_components
245 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
246 | #bower_components/
247 |
248 | # RIA/Silverlight projects
249 | Generated_Code/
250 |
251 | # Backup & report files from converting an old project file
252 | # to a newer Visual Studio version. Backup files are not needed,
253 | # because we have git ;-)
254 | _UpgradeReport_Files/
255 | Backup*/
256 | UpgradeLog*.XML
257 | UpgradeLog*.htm
258 | ServiceFabricBackup/
259 | *.rptproj.bak
260 |
261 | # SQL Server files
262 | *.mdf
263 | *.ldf
264 | *.ndf
265 |
266 | # Business Intelligence projects
267 | *.rdl.data
268 | *.bim.layout
269 | *.bim_*.settings
270 | *.rptproj.rsuser
271 | *- [Bb]ackup.rdl
272 | *- [Bb]ackup ([0-9]).rdl
273 | *- [Bb]ackup ([0-9][0-9]).rdl
274 |
275 | # Microsoft Fakes
276 | FakesAssemblies/
277 |
278 | # GhostDoc plugin setting file
279 | *.GhostDoc.xml
280 |
281 | # Node.js Tools for Visual Studio
282 | .ntvs_analysis.dat
283 | node_modules/
284 |
285 | # Visual Studio 6 build log
286 | *.plg
287 |
288 | # Visual Studio 6 workspace options file
289 | *.opt
290 |
291 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
292 | *.vbw
293 |
294 | # Visual Studio LightSwitch build output
295 | **/*.HTMLClient/GeneratedArtifacts
296 | **/*.DesktopClient/GeneratedArtifacts
297 | **/*.DesktopClient/ModelManifest.xml
298 | **/*.Server/GeneratedArtifacts
299 | **/*.Server/ModelManifest.xml
300 | _Pvt_Extensions
301 |
302 | # Paket dependency manager
303 | .paket/paket.exe
304 | paket-files/
305 |
306 | # FAKE - F# Make
307 | .fake/
308 |
309 | # CodeRush personal settings
310 | .cr/personal
311 |
312 | # Python Tools for Visual Studio (PTVS)
313 | __pycache__/
314 | *.pyc
315 |
316 | # Cake - Uncomment if you are using it
317 | # tools/**
318 | # !tools/packages.config
319 |
320 | # Tabs Studio
321 | *.tss
322 |
323 | # Telerik's JustMock configuration file
324 | *.jmconfig
325 |
326 | # BizTalk build output
327 | *.btp.cs
328 | *.btm.cs
329 | *.odx.cs
330 | *.xsd.cs
331 |
332 | # OpenCover UI analysis results
333 | OpenCover/
334 |
335 | # Azure Stream Analytics local run output
336 | ASALocalRun/
337 |
338 | # MSBuild Binary and Structured Log
339 | *.binlog
340 |
341 | # NVidia Nsight GPU debugger configuration file
342 | *.nvuser
343 |
344 | # MFractors (Xamarin productivity tool) working folder
345 | .mfractor/
346 |
347 | # Local History for Visual Studio
348 | .localhistory/
349 |
350 | # BeatPulse healthcheck temp database
351 | healthchecksdb
352 |
353 | # Backup folder for Package Reference Convert tool in Visual Studio 2017
354 | MigrationBackup/
355 |
356 | thirdparty/
--------------------------------------------------------------------------------
/Cafe.Matcha/Cafe.Matcha.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 | net48
4 | 8.0
5 | Library
6 | 10.0.10240.0
7 |
8 | true
9 | true
10 | true
11 | true
12 | false
13 |
14 |
15 | FFCafe
16 | Copyright © FFCafe $([System.DateTime]::UtcNow.Year)
17 | $([System.DateTime]::UtcNow.ToString("yy.M.d.Hmm"))
18 |
19 |
20 | ..\bin\Debug\
21 |
22 |
23 | true
24 | ..\bin\Release\
25 |
26 | MinimumRecommendedRules.ruleset
27 |
28 |
29 | OnBuildSuccess
30 |
31 |
32 |
33 | ..\thirdparty\ACT\Advanced Combat Tracker.dll
34 | ..\thirdparty\ACT\Advanced Combat Tracker.exe
35 | False
36 |
37 |
38 | ..\thirdparty\FFXIV_ACT_Plugin\FFXIV_ACT_Plugin.dll
39 | False
40 |
41 |
42 | ..\thirdparty\FFXIV_ACT_Plugin\SDK\FFXIV_ACT_Plugin.Common.dll
43 | False
44 |
45 |
46 | ..\thirdparty\FFXIV_ACT_Plugin\SDK\FFXIV_ACT_Plugin.Memory.dll
47 | False
48 |
49 |
50 | ..\thirdparty\FFXIV_ACT_Plugin\SDK\FFXIV_ACT_Plugin.Network.dll
51 | False
52 |
53 |
54 | False
55 |
56 |
57 |
58 | C:\Program Files (x86)\Windows Kits\10\References\10.0.26100.0\Windows.Foundation.UniversalApiContract\19.0.0.0\Windows.Foundation.UniversalApiContract.winmd
59 | C:\Program Files (x86)\Windows Kits\10\References\10.0.22621.0\Windows.Foundation.UniversalApiContract\15.0.0.0\Windows.Foundation.UniversalApiContract.winmd
60 | C:\Program Files (x86)\Windows Kits\10\References\10.0.19041.0\Windows.Foundation.UniversalApiContract\10.0.0.0\Windows.Foundation.UniversalApiContract.winmd
61 | C:\Program Files (x86)\Windows Kits\10\References\10.0.18362.0\Windows.Foundation.UniversalApiContract\8.0.0.0\Windows.Foundation.UniversalApiContract.winmd
62 | C:\Program Files (x86)\Windows Kits\10\References\10.0.17763.0\Windows.Foundation.UniversalApiContract\7.0.0.0\Windows.Foundation.UniversalApiContract.winmd
63 | False
64 |
65 |
66 |
67 |
68 |
69 | Always
70 |
71 |
72 | Always
73 |
74 |
75 | Always
76 |
77 |
78 | Always
79 |
80 |
81 | Always
82 |
83 |
84 | Always
85 |
86 |
87 | Always
88 |
89 |
90 | Always
91 |
92 |
93 | Always
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 | 1.6.0
109 | runtime; build; native; contentfiles; analyzers; buildtransitive
110 | all
111 |
112 |
113 | 5.7.0
114 | runtime; build; native; contentfiles; analyzers; buildtransitive
115 | all
116 |
117 |
118 | 6.6.4
119 | runtime; build; native; contentfiles; analyzers; buildtransitive
120 | all
121 |
122 |
123 | 13.0.2
124 |
125 |
126 | 4.1.0
127 | all
128 |
129 |
130 | 1.0.0
131 |
132 |
133 | 0.4.0
134 |
135 |
136 | 6.0.1
137 |
138 |
139 |
140 |
141 |
142 |
143 |
--------------------------------------------------------------------------------
/Cafe.Matcha/data/roulette.json:
--------------------------------------------------------------------------------
1 | {
2 | "1": {
3 | "chs": "随机任务:练级迷宫",
4 | "en": "Duty Roulette: Leveling",
5 | "ja": "コンテンツルーレット:レべリング",
6 | "de": "Zufallsinhalt: Stufensteigerung",
7 | "fr": "Mission aléatoire : gain de niveaux"
8 | },
9 | "2": {
10 | "chs": "随机任务:拾级迷宫",
11 | "en": "Duty Roulette: High-level Dungeons",
12 | "ja": "コンテンツルーレット:ハイレベリング",
13 | "de": "Zufallsinhalt: Hohe Stufen",
14 | "fr": "Mission aléatoire : donjons avancés"
15 | },
16 | "3": {
17 | "chs": "随机任务:主线任务",
18 | "en": "Duty Roulette: Main Scenario",
19 | "ja": "コンテンツルーレット:メインクエスト",
20 | "de": "Zufallsinhalt: Hauptszenario",
21 | "fr": "Mission aléatoire : épopée"
22 | },
23 | "4": {
24 | "chs": "随机任务:行会令",
25 | "en": "Duty Roulette: Guildhests",
26 | "ja": "コンテンツルーレット:ギルドオーダー",
27 | "de": "Zufallsinhalt: Gildengeheiß",
28 | "fr": "Mission aléatoire : opérations de guilde"
29 | },
30 | "5": {
31 | "chs": "随机任务:顶级迷宫",
32 | "en": "Duty Roulette: Expert",
33 | "ja": "コンテンツルーレット:エキスパート",
34 | "de": "Zufallsinhalt: Experte",
35 | "fr": "Mission aléatoire : expert"
36 | },
37 | "6": {
38 | "chs": "随机任务:讨伐歼灭战",
39 | "en": "Duty Roulette: Trials",
40 | "ja": "コンテンツルーレット:討伐・討滅戦",
41 | "de": "Zufallsinhalt: Prüfung",
42 | "fr": "Mission aléatoire : défis"
43 | },
44 | "7": {
45 | "chs": "每日挑战:纷争前线",
46 | "en": "Daily Challenge: Frontline",
47 | "ja": "デイリーチャレンジ:フロントライン",
48 | "de": "Tagesherausforderung: PvP-Front",
49 | "fr": "Challenge quotidien : Front"
50 | },
51 | "8": {
52 | "chs": "随机任务:满级迷宫",
53 | "en": "Duty Roulette: Level Cap Dungeons",
54 | "ja": "コンテンツルーレット:レベルキャップダンジョン",
55 | "de": "Zufallsinhalt: Höchststufe",
56 | "fr": "Mission aléatoire : donjons supérieurs"
57 | },
58 | "9": {
59 | "chs": "随机任务:指导者任务",
60 | "en": "Duty Roulette: Mentor",
61 | "ja": "コンテンツルーレット:メンター",
62 | "de": "Zufallsinhalt: Mentor",
63 | "fr": "Mission aléatoire : mentor"
64 | },
65 | "15": {
66 | "chs": "随机任务:团队任务",
67 | "en": "Duty Roulette: Alliance Raids",
68 | "ja": "コンテンツルーレット:アライアンスレイド",
69 | "de": "Zufallsinhalt: Allianz-Raid",
70 | "fr": "Mission aléatoire : raids en alliance"
71 | },
72 | "17": {
73 | "chs": "随机任务:大型任务",
74 | "en": "Duty Roulette: Normal Raids",
75 | "ja": "コンテンツルーレット:ノーマルレイド",
76 | "de": "Zufallsinhalt: Normaler Raid",
77 | "fr": "Mission aléatoire : raids normaux"
78 | },
79 | "18": {
80 | "chs": "陆行鸟竞赛:荒野大道",
81 | "en": "Chocobo Race: Sagolii Road",
82 | "ja": "チョコボレース:サゴリーロード",
83 | "de": "Chocobo-Rennen: Sagolii-Straße",
84 | "fr": "Course de chocobos : Route de Sagolii"
85 | },
86 | "19": {
87 | "chs": "陆行鸟竞赛:太阳海岸",
88 | "en": "Chocobo Race: Costa del Sol",
89 | "ja": "チョコボレース:コスタ・デル・ソル",
90 | "de": "Chocobo-Rennen: Sonnenküste",
91 | "fr": "Course de chocobos : Costa del Sol"
92 | },
93 | "20": {
94 | "chs": "陆行鸟竞赛:恬静小路",
95 | "en": "Chocobo Race: Tranquil Paths",
96 | "ja": "チョコボレース:トランキルパス",
97 | "de": "Chocobo-Rennen: Pfad der Seelenruhe",
98 | "fr": "Course de chocobos : Sentes tranquilles"
99 | },
100 | "21": {
101 | "chs": "陆行鸟竞赛:随机赛道",
102 | "en": "Chocobo Race: Random",
103 | "ja": "チョコボレース:コースルーレット",
104 | "de": "Chocobo-Rennen: Zufallsstrecke",
105 | "fr": "Course de chocobos : aléatoire"
106 | },
107 | "22": {
108 | "chs": "陆行鸟竞赛:荒野大道(无报酬)",
109 | "en": "Chocobo Race: Sagolii Road (No Rewards)",
110 | "ja": "チョコボレース:サゴリーロード (報酬なし)",
111 | "de": "Chocobo-Rennen: Sagolii-Straße (keine Belohnung)",
112 | "fr": "Course de chocobos : Route de Sagolii (sans récompense)"
113 | },
114 | "23": {
115 | "chs": "陆行鸟竞赛:太阳海岸(无报酬)",
116 | "en": "Chocobo Race: Costa del Sol (No Rewards)",
117 | "ja": "チョコボレース:コスタ・デル・ソル (報酬なし)",
118 | "de": "Chocobo-Rennen: Sonnenküste (keine Belohnung)",
119 | "fr": "Course de chocobos : Costa del Sol (sans récompense)"
120 | },
121 | "24": {
122 | "chs": "陆行鸟竞赛:恬静小路(无报酬)",
123 | "en": "Chocobo Race: Tranquil Paths (No Rewards)",
124 | "ja": "チョコボレース:トランキルパス (報酬なし)",
125 | "de": "Chocobo-Rennen: Pfad der Seelenruhe (keine Belohnung)",
126 | "fr": "Course de chocobos : Sentes tranquilles (sans récompense)"
127 | },
128 | "25": {
129 | "chs": "陆行鸟竞赛:随机赛道(无报酬)",
130 | "en": "Chocobo Race: Random (No Rewards)",
131 | "ja": "チョコボレース:コースルーレット (報酬なし)",
132 | "de": "Chocobo-Rennen: Zufallsstrecke (keine Belohnung)",
133 | "fr": "Course de chocobos : aléatoire (sans récompense)"
134 | },
135 | "26": {
136 | "chs": "陆行鸟竞赛:随机赛道",
137 | "en": "Chocobo Race: Random",
138 | "ja": "チョコボレース:コースルーレット",
139 | "de": "Chocobo-Rennen: Zufallsstrecke",
140 | "fr": "Course de chocobos : aléatoire"
141 | },
142 | "27": {
143 | "chs": "陆行鸟竞赛:随机赛道",
144 | "en": "Chocobo Race: Random",
145 | "ja": "チョコボレース:コースルーレット",
146 | "de": "Chocobo-Rennen: Zufallsstrecke",
147 | "fr": "Course de chocobos : aléatoire"
148 | },
149 | "28": {
150 | "chs": "陆行鸟竞赛:随机赛道",
151 | "en": "Chocobo Race: Random",
152 | "ja": "チョコボレース:コースルーレット",
153 | "de": "Chocobo-Rennen: Zufallsstrecke",
154 | "fr": "Course de chocobos : aléatoire"
155 | },
156 | "29": {
157 | "chs": "陆行鸟竞赛:随机赛道",
158 | "en": "Chocobo Race: Random",
159 | "ja": "チョコボレース:コースルーレット",
160 | "de": "Chocobo-Rennen: Zufallsstrecke",
161 | "fr": "Course de chocobos : aléatoire"
162 | },
163 | "30": {
164 | "chs": "陆行鸟竞赛:随机赛道",
165 | "en": "Chocobo Race: Random",
166 | "ja": "チョコボレース:コースルーレット",
167 | "de": "Chocobo-Rennen: Zufallsstrecke",
168 | "fr": "Course de chocobos : aléatoire"
169 | },
170 | "31": {
171 | "chs": "陆行鸟竞赛:随机赛道",
172 | "en": "Chocobo Race: Random",
173 | "ja": "チョコボレース:コースルーレット",
174 | "de": "Chocobo-Rennen: Zufallsstrecke",
175 | "fr": "Course de chocobos : aléatoire"
176 | },
177 | "32": {
178 | "chs": "陆行鸟竞赛:随机赛道",
179 | "en": "Chocobo Race: Random",
180 | "ja": "チョコボレース:コースルーレット",
181 | "de": "Chocobo-Rennen: Zufallsstrecke",
182 | "fr": "Course de chocobos : aléatoire"
183 | },
184 | "33": {
185 | "chs": "陆行鸟竞赛:随机赛道",
186 | "en": "Chocobo Race: Random",
187 | "ja": "チョコボレース:コースルーレット",
188 | "de": "Chocobo-Rennen: Zufallsstrecke",
189 | "fr": "Course de chocobos : aléatoire"
190 | },
191 | "34": {
192 | "chs": "陆行鸟竞赛:随机赛道",
193 | "en": "Chocobo Race: Random",
194 | "ja": "チョコボレース:コースルーレット",
195 | "de": "Chocobo-Rennen: Zufallsstrecke",
196 | "fr": "Course de chocobos : aléatoire"
197 | },
198 | "35": {
199 | "chs": "陆行鸟竞赛:随机赛道",
200 | "en": "Chocobo Race: Random",
201 | "ja": "チョコボレース:コースルーレット",
202 | "de": "Chocobo-Rennen: Zufallsstrecke",
203 | "fr": "Course de chocobos : aléatoire"
204 | },
205 | "36": {
206 | "chs": "陆行鸟竞赛:随机赛道",
207 | "en": "Chocobo Race: Random",
208 | "ja": "チョコボレース:コースルーレット",
209 | "de": "Chocobo-Rennen: Zufallsstrecke",
210 | "fr": "Course de chocobos : aléatoire"
211 | },
212 | "37": {
213 | "chs": "陆行鸟竞赛:随机赛道",
214 | "en": "Chocobo Race: Random",
215 | "ja": "チョコボレース:コースルーレット",
216 | "de": "Chocobo-Rennen: Zufallsstrecke",
217 | "fr": "Course de chocobos : aléatoire"
218 | },
219 | "38": {
220 | "chs": "陆行鸟竞赛:随机赛道",
221 | "en": "Chocobo Race: Random",
222 | "ja": "チョコボレース:コースルーレット",
223 | "de": "Chocobo-Rennen: Zufallsstrecke",
224 | "fr": "Course de chocobos : aléatoire"
225 | },
226 | "40": {
227 | "chs": "水晶冲突(练习赛)",
228 | "en": "Crystalline Conflict (Casual Match)",
229 | "ja": "クリスタルコンフリクト(カジュアルマッチ)",
230 | "de": "Crystalline Conflict: Freies Spiel",
231 | "fr": "Crystalline Conflict (partie non classée)"
232 | },
233 | "41": {
234 | "chs": "水晶冲突(段位赛)",
235 | "en": "Crystalline Conflict (Ranked Match)",
236 | "ja": "クリスタルコンフリクト(ランクマッチ)",
237 | "de": "Crystalline Conflict: Gewertetes Spiel",
238 | "fr": "Crystalline Conflict (partie classée)"
239 | }
240 | }
--------------------------------------------------------------------------------