Skip to content

Commit

Permalink
Add Recipes API
Browse files Browse the repository at this point in the history
  • Loading branch information
Citrinate committed Jan 3, 2025
1 parent 12e4613 commit e997965
Show file tree
Hide file tree
Showing 7 changed files with 280 additions and 8 deletions.
6 changes: 4 additions & 2 deletions CS2Interface/GameData/GameData.cs
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,10 @@ internal static void Update(bool forceUpdate = false) {
UpdateTimer.Change(TimeSpan.FromTicks(0), TimeSpan.FromSeconds(10));
}

internal static async Task<bool> IsLoaded(uint maxWaitTimeSeconds = 60) {
Update();
internal static async Task<bool> IsLoaded(uint maxWaitTimeSeconds = 60, bool update = true) {
if (update) {
Update();
}

DateTime timeoutTime = DateTime.Now.AddSeconds(maxWaitTimeSeconds);
while (IsUpdating) {
Expand Down
30 changes: 30 additions & 0 deletions CS2Interface/GameData/GameDataText.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using ArchiSteamFarm.Core;
using SteamKit2;
Expand Down Expand Up @@ -34,5 +35,34 @@ internal string? this[string? key] {
return Data.Where(x => x.Name?.ToUpper().Trim() == key.ToUpper().Trim()).FirstOrDefault()?.Value;
}
}

internal string? Format(string? key, params string?[] inserts) {
string? value = this[key];
if (value == null) {
return null;
}

// Convert strings used in C++ printf into strings we can use for C# String.Format
// Ex: "%s1 %s2 %s3" to "{0} {1} {2}"
// Only care about the %s format specifier, which can only go up to %s9? https://github.com/nillerusr/source-engine/blob/29985681a18508e78dc79ad863952f830be237b6/tier1/ilocalize.cpp#L74

try {
StringBuilder sb = new(value);

sb.Replace("%s1", "{0}");
sb.Replace("%s2", "{1}");
sb.Replace("%s3", "{2}");
sb.Replace("%s4", "{3}");
sb.Replace("%s5", "{4}");
sb.Replace("%s6", "{5}");
sb.Replace("%s7", "{6}");
sb.Replace("%s8", "{7}");
sb.Replace("%s9", "{8}");

return String.Format(sb.ToString(), inserts);
} catch {
return null;
}
}
}
}
104 changes: 104 additions & 0 deletions CS2Interface/GameData/GameObjects/Recipes/Recipe.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
using System.Collections.Generic;
using System.Linq;
using System.Text.Json.Serialization;
using System.Threading.Tasks;
using CS2Interface.Localization;
using SteamKit2;

namespace CS2Interface {
public class Recipe : GameObject {
[JsonInclude]
[JsonPropertyName("id")]
public ushort RecipeID { get; private init; }

[JsonInclude]
[JsonPropertyName("name")]
public string? Name { get; private set; }

[JsonInclude]
[JsonPropertyName("inputs")]
public string? InputDescription { get; private set; }

[JsonInclude]
[JsonPropertyName("outputs")]
public string? OutputDescription { get; private set; }

[JsonInclude]
[JsonPropertyName("quality")]
public string? Quality { get; private set; }

[JsonInclude]
[JsonPropertyName("defs")]
[JsonConverter(typeof(KVConverter))]
public KeyValue? RecipeData { get; private set; }

public bool ShouldSerializeRecipeID() => true;
public bool ShouldSerializeName() => Name != null && ShouldSerializeAdditionalProperties;
public bool ShouldSerializeInputDescription() => InputDescription != null && ShouldSerializeAdditionalProperties;
public bool ShouldSerializeOutputDescription() => OutputDescription != null && ShouldSerializeAdditionalProperties;
public bool ShouldSerializeQuality() => Quality != null && ShouldSerializeAdditionalProperties;
public bool ShouldSerializeRecipeData() => RecipeData != null && ShouldSerializeDefs;

public Recipe(ushort recipeID) {
RecipeID = recipeID;

SetDefs();
SetAdditionalProperties();
}

protected override bool SetDefs() {
KeyValue? recipeDef = GameData.ItemsGame.GetDef("recipes", RecipeID.ToString());
if (recipeDef == null) {
return false;
}

RecipeData = recipeDef.Clone();

return true;
}

protected override bool SetAdditionalProperties() {
if (RecipeData == null) {
return false;
}

Name = GameData.CsgoEnglish.Format(RecipeData["name"].Value?.Substring(1), GameData.CsgoEnglish[RecipeData["n_A"].Value?.Substring(1)]);
InputDescription = GameData.CsgoEnglish.Format(RecipeData["desc_inputs"].Value?.Substring(1), RecipeData["di_A"].Value, GameData.CsgoEnglish[RecipeData["di_B"].Value?.Substring(1)]);
OutputDescription = GameData.CsgoEnglish.Format(RecipeData["desc_outputs"].Value?.Substring(1), RecipeData["do_A"].Value, GameData.CsgoEnglish[RecipeData["do_B"].Value?.Substring(1)]);

{
List<KeyValue>? inputConditions = RecipeData["input_items"].Children.FirstOrDefault()?["conditions"].Children;
if (inputConditions != null) {
foreach (KeyValue condition in inputConditions) {
if (condition["field"].Value == "*quality") {
Quality = GameData.CsgoEnglish[condition["value"].Value];
break;
}
}
}
}

return true;
}

public static async Task<List<Recipe>> GetAll() {
if (!await GameData.IsLoaded(update: false).ConfigureAwait(false)) {
throw new ClientException(EClientExceptionType.Failed, Strings.GameDataLoadingFailed);
}

List<KeyValue>? kvs = GameData.ItemsGame["recipes"];
if (kvs == null) {
return [];
}

List<Recipe> recipes = [];
foreach (KeyValue kv in kvs) {
if (ushort.TryParse(kv.Name, out ushort recipeID)) {
recipes.Add(new Recipe(recipeID));
}
}

return recipes;
}
}
}
17 changes: 17 additions & 0 deletions CS2Interface/IPC/Api/CS2InterfaceController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -330,6 +330,23 @@ public async Task<ActionResult<GenericResponse>> CraftItem(string botName, ushor
return Ok(new GenericResponse<GCMsg.MsgCraftResponse>(true, craftResponse));
}

[HttpGet("Recipes")]
[SwaggerOperation (Summary = "Get a list of crafting recipes")]
[ProducesResponseType(typeof(GenericResponse<List<Recipe>>), (int) HttpStatusCode.OK)]
[ProducesResponseType(typeof(GenericResponse), (int) HttpStatusCode.BadRequest)]
public async Task<ActionResult<GenericResponse>> Recipes([FromQuery] bool showDefs = false) {
List<Recipe> recipes;
try {
recipes = await Recipe.GetAll().ConfigureAwait(false);
} catch (ClientException e) {
return BadRequest(new GenericResponse(false, e.Message));
}

GameObject.SetSerializationProperties(true, showDefs);

return Ok(new GenericResponse<List<Recipe>>(true, recipes));
}

private async Task<ActionResult<GenericResponse>> HandleClientException(Bot bot, ClientException e) {
bot.ArchiLogger.LogGenericError(e.Message);
if (e.Type == EClientExceptionType.Timeout) {
Expand Down
110 changes: 110 additions & 0 deletions CS2Interface/IPC/Documentation/Data/Recipes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
# GET /Api/CS2Interface/Recipes

## Description

Get a list of crafting recipes currently in the game

## Path Parameters

None

## Query Parameters

Name | Required | Description
--- | --- | ---
`showDefs` | No | If set to true, the response will include a `defs` property containing additional game data

## Response Result

Property | Type | Description
--- | --- | ---
`id` | `ushort` | The recipe ID
`name` | `string` | The name of the crafting recipe
`inputs` | `string` | A description of the inputs
`outputs` | `string` | A description of the outputs
`quality` | `string` | The quality of the inputs and outputs (Unique or StatTrak™)

## Example Response

```
http://127.0.0.1:1242/Api/CS2Interface/Recipes
```

```json
{
"Message": "OK",
"Success": true,
"Result": [
{
"id": 0,
"name": "Trade Up Consumer-Grade Weapons",
"inputs": "Requires: 10 Consumer-Grade Weapons",
"outputs": "Produces: 1 Industrial-Grade Weapon",
"quality": "Unique"
},
{
"id": 1,
"name": "Trade Up Industrial-Grade Weapons",
"inputs": "Requires: 10 Industrial-Grade Weapons",
"outputs": "Produces: 1 Mil-Spec Weapon",
"quality": "Unique"
},
{
"id": 2,
"name": "Trade Up Mil-Spec Weapons",
"inputs": "Requires: 10 Mil-Spec Weapons",
"outputs": "Produces: 1 Restricted Weapon",
"quality": "Unique"
},
{
"id": 3,
"name": "Trade Up Restricted Weapons",
"inputs": "Requires: 10 Restricted Weapons",
"outputs": "Produces: 1 Classified Weapon",
"quality": "Unique"
},
{
"id": 4,
"name": "Trade Up Classified Weapons",
"inputs": "Requires: 10 Classified Weapons",
"outputs": "Produces: 1 Covert Weapon",
"quality": "Unique"
},
{
"id": 10,
"name": "Trade Up Consumer-Grade Weapons",
"inputs": "Requires: 10 Consumer-Grade Weapons",
"outputs": "Produces: 1 Industrial-Grade Weapon",
"quality": "StatTrak™"
},
{
"id": 11,
"name": "Trade Up Industrial-Grade Weapons",
"inputs": "Requires: 10 Industrial-Grade Weapons",
"outputs": "Produces: 1 Mil-Spec Weapon",
"quality": "StatTrak™"
},
{
"id": 12,
"name": "Trade Up Mil-Spec Weapons",
"inputs": "Requires: 10 Mil-Spec Weapons",
"outputs": "Produces: 1 Restricted Weapon",
"quality": "StatTrak™"
},
{
"id": 13,
"name": "Trade Up Restricted Weapons",
"inputs": "Requires: 10 Restricted Weapons",
"outputs": "Produces: 1 Classified Weapon",
"quality": "StatTrak™"
},
{
"id": 14,
"name": "Trade Up Classified Weapons",
"inputs": "Requires: 10 Classified Weapons",
"outputs": "Produces: 1 Covert Weapon",
"quality": "StatTrak™"
}
]
}
```
3 changes: 3 additions & 0 deletions CS2Interface/IPC/Documentation/Items/CraftItem.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ Name | Required | Description
`botName` | Yes | The ASF bot name that will be used to craft an item
`recipeID` | Yes | The ID for the crafting recipe

> [!NOTE]
> A list of `recipeIDs` can be found using the [Recipes](/CS2Interface/IPC/Documentation/Data/Recipes.md) API
## Query Parameters

Name | Required | Description
Expand Down
18 changes: 12 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,12 +63,6 @@ API | Method | Parameters | Description
[`/Api/CS2Interface/{botNames}/Status`](CS2Interface/IPC/Documentation/Interface/Status.md)|`GET`| |Get the CS2 Interface status
[`/Api/CS2Interface/{botNames}/Stop`](CS2Interface/IPC/Documentation/Interface/Stop.md)|`GET`| |Stops the CS2 Interface

#### Players

API | Method | Parameters | Description
--- | --- | --- | ---
[`/Api/CS2Interface/{botName}/PlayerProfile/{steamID}`](CS2Interface/IPC/Documentation/Players/PlayerProfile.md)|`GET`| |Get a friend's player profile

#### Items

API | Method | Parameters | Description
Expand All @@ -79,3 +73,15 @@ API | Method | Parameters | Description
[`/Api/CS2Interface/{botName}/Inventory`](CS2Interface/IPC/Documentation/Items/Inventory.md)|`GET`|`minimal`, `showDefs`|Get a bot's inventory
[`/Api/CS2Interface/{botName}/RetrieveItem/{crateID}/{itemID}`](CS2Interface/IPC/Documentation/Items/RetrieveItem.md)|`GET`| |Take an item out of a storage unit
[`/Api/CS2Interface/{botName}/StoreItem/{crateID}/{itemID}`](CS2Interface/IPC/Documentation/Items/StoreItem.md)|`GET`| |Place an item into a storage unit

#### Players

API | Method | Parameters | Description
--- | --- | --- | ---
[`/Api/CS2Interface/{botName}/PlayerProfile/{steamID}`](CS2Interface/IPC/Documentation/Players/PlayerProfile.md)|`GET`| |Get a friend's player profile

#### Data

API | Method | Parameters | Description
--- | --- | --- | ---
[`/Api/CS2Interface/Recipes/`](CS2Interface/IPC/Documentation/Data/Recipes.md)|`GET`|`showDefs`|Get a list of crafting recipes

0 comments on commit e997965

Please sign in to comment.