diff --git a/Source/RIMAPI/RimworldRestApi/Controllers/Pawns/PawnSocialController.cs b/Source/RIMAPI/RimworldRestApi/Controllers/Pawns/PawnSocialController.cs new file mode 100644 index 0000000..672cf13 --- /dev/null +++ b/Source/RIMAPI/RimworldRestApi/Controllers/Pawns/PawnSocialController.cs @@ -0,0 +1,101 @@ +using System.Net; +using System.Threading.Tasks; +using RIMAPI.Core; +using RIMAPI.Http; +using RIMAPI.Models; +using RIMAPI.Services; + +namespace RIMAPI.Controllers +{ + public class PawnSocialController + { + private readonly IPawnSocialService _socialService; + + public PawnSocialController(IPawnSocialService socialService) + { + _socialService = socialService; + } + + [Get("/api/v1/game/defs/interactions")] + [EndpointMetadata("Retrieves a list of all valid InteractionDef names in the game.")] + public async Task GetInteractionDefs(HttpListenerContext context) + { + var result = _socialService.GetInteractionDefs(); + await context.SendJsonResponse(result); + } + + [Get("/api/v1/pawns/interactions")] + [EndpointMetadata("Gets the real-time interaction readiness of a specific pawn.")] + public async Task GetPawnInteractionStatus(HttpListenerContext context) + { + var pawnId = RequestParser.GetIntParameter(context, "pawn_id"); + var result = _socialService.GetPawnInteractionStatus(pawnId); + await context.SendJsonResponse(result); + } + + [Get("/api/v1/pawns/interactions/log")] + [EndpointMetadata("Retrieves the recent interaction history for a specific pawn.")] + public async Task GetPawnInteractionLog(HttpListenerContext context) + { + var pawnId = RequestParser.GetIntParameter(context, "pawn_id"); + var limit = RequestParser.HasParameter(context, "limit") ? RequestParser.GetIntParameter(context, "limit") : 50; + var result = _socialService.GetPawnInteractionLog(pawnId, limit); + await context.SendJsonResponse(result); + } + + [Get("/api/v1/pawns/relations")] + [EndpointMetadata("Retrieves all established familial and romantic relationships for this pawn.")] + public async Task GetPawnRelations(HttpListenerContext context) + { + var pawnId = RequestParser.GetIntParameter(context, "pawn_id"); + var result = _socialService.GetPawnRelations(pawnId); + await context.SendJsonResponse(result); + } + + [Get("/api/v1/pawns/opinions")] + [EndpointMetadata("Retrieves a list of this pawn's numerical opinion toward every other colonist on the map.")] + public async Task GetPawnOpinions(HttpListenerContext context) + { + var pawnId = RequestParser.GetIntParameter(context, "pawn_id"); + var result = _socialService.GetPawnOpinions(pawnId); + await context.SendJsonResponse(result); + } + + [Post("/api/v1/pawns/interactions/force")] + [EndpointMetadata("Forces a specific social interaction to occur immediately between two pawns.")] + public async Task ForceInteraction(HttpListenerContext context) + { + var request = await context.Request.ReadBodyAsync(); + var result = _socialService.ForceInteraction(request); + await context.SendJsonResponse(result); + } + + [Post("/api/v1/pawns/relations/add")] + [EndpointMetadata("Instantly creates a permanent social bond between two pawns.")] + public async Task AddRelation(HttpListenerContext context) + { + var request = await context.Request.ReadBodyAsync(); + var result = _socialService.AddRelation(request); + await context.SendJsonResponse(result); + } + + [Delete("/api/v1/pawns/relations/remove")] + [EndpointMetadata("Severs a specific social bond between two pawns.")] + public async Task RemoveRelation(HttpListenerContext context) + { + var pawn1Id = RequestParser.GetIntParameter(context, "pawn1_id"); + var pawn2Id = RequestParser.GetIntParameter(context, "pawn2_id"); + var relationDefName = RequestParser.GetStringParameter(context, "relation_def_name"); + + var request = new RemoveRelationRequestDto + { + Pawn1Id = pawn1Id, + Pawn2Id = pawn2Id, + RelationDefName = relationDefName + }; + + var result = _socialService.RemoveRelation(request); + await context.SendJsonResponse(result); + } + } +} diff --git a/Source/RIMAPI/RimworldRestApi/Models/Pawns/PawnSocialDto.cs b/Source/RIMAPI/RimworldRestApi/Models/Pawns/PawnSocialDto.cs new file mode 100644 index 0000000..78ba413 --- /dev/null +++ b/Source/RIMAPI/RimworldRestApi/Models/Pawns/PawnSocialDto.cs @@ -0,0 +1,90 @@ +using System.Collections.Generic; + +namespace RIMAPI.Models +{ + public class InteractionDefDto + { + public string DefName { get; set; } + public string Label { get; set; } + public string Description { get; set; } + } + + public class PawnInteractionStatusDto + { + public bool CanInteract { get; set; } + public int CooldownTicks { get; set; } + public float CooldownDays { get; set; } + public string LastInteractionDef { get; set; } + public int LastInteractionTicks { get; set; } + } + + public class PawnInteractionLogDto + { + public int PawnId { get; set; } + public List Interactions { get; set; } + public int Count { get; set; } + } + + public class InteractionLogEntryDto + { + public int InitiatorId { get; set; } + public string InitiatorName { get; set; } + public int RecipientId { get; set; } + public string RecipientName { get; set; } + public string InteractionDefName { get; set; } + public string InteractionLabel { get; set; } + public string Text { get; set; } + public int Ticks { get; set; } + public string TimeAgo { get; set; } + } + + public class PawnOpinionDto + { + public int TargetPawnId { get; set; } + public string TargetPawnName { get; set; } + public int Opinion { get; set; } + public List Breakdown { get; set; } + } + + public class OpinionBreakdownDto + { + public string ThoughtDefName { get; set; } + public string Label { get; set; } + public float Score { get; set; } + } + + public class ForceInteractionRequestDto + { + public int InitiatorId { get; set; } + public int RecipientId { get; set; } + public string InteractionDefName { get; set; } + } + + public class AddRelationRequestDto + { + public int Pawn1Id { get; set; } + public int Pawn2Id { get; set; } + public string RelationDefName { get; set; } + } + + public class RemoveRelationRequestDto + { + public int Pawn1Id { get; set; } + public int Pawn2Id { get; set; } + public string RelationDefName { get; set; } + } + + public class PawnRelationsDto + { + public int PawnId { get; set; } + public List Relations { get; set; } + } + + public class PawnRelationEntryDto + { + public int OtherPawnId { get; set; } + public string OtherPawnName { get; set; } + public string RelationDefName { get; set; } + public string RelationLabel { get; set; } + } +} diff --git a/Source/RIMAPI/RimworldRestApi/Services/Pawns/PawnSocialService/IPawnSocialService.cs b/Source/RIMAPI/RimworldRestApi/Services/Pawns/PawnSocialService/IPawnSocialService.cs new file mode 100644 index 0000000..b442a23 --- /dev/null +++ b/Source/RIMAPI/RimworldRestApi/Services/Pawns/PawnSocialService/IPawnSocialService.cs @@ -0,0 +1,18 @@ +using System.Collections.Generic; +using RIMAPI.Core; +using RIMAPI.Models; + +namespace RIMAPI.Services +{ + public interface IPawnSocialService + { + ApiResult> GetInteractionDefs(); + ApiResult GetPawnInteractionStatus(int pawnId); + ApiResult GetPawnInteractionLog(int pawnId, int limit = 50); + ApiResult GetPawnRelations(int pawnId); + ApiResult> GetPawnOpinions(int pawnId); + ApiResult ForceInteraction(ForceInteractionRequestDto request); + ApiResult AddRelation(AddRelationRequestDto request); + ApiResult RemoveRelation(RemoveRelationRequestDto request); + } +} diff --git a/Source/RIMAPI/RimworldRestApi/Services/Pawns/PawnSocialService/PawnSocialService.cs b/Source/RIMAPI/RimworldRestApi/Services/Pawns/PawnSocialService/PawnSocialService.cs new file mode 100644 index 0000000..9713bba --- /dev/null +++ b/Source/RIMAPI/RimworldRestApi/Services/Pawns/PawnSocialService/PawnSocialService.cs @@ -0,0 +1,423 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using HarmonyLib; +using RIMAPI.Core; +using RIMAPI.Helpers; +using RIMAPI.Models; +using RimWorld; +using Verse; + +namespace RIMAPI.Services +{ + public class PawnSocialService : IPawnSocialService + { + public ApiResult> GetInteractionDefs() + { + try + { + var defs = DefDatabase.AllDefsListForReading + .Select(d => new InteractionDefDto + { + DefName = d.defName, + Label = d.label, + Description = d.description + }) + .ToList(); + return ApiResult>.Ok(defs); + } + catch (Exception ex) + { + return ApiResult>.Fail(ex.Message); + } + } + + public ApiResult GetPawnInteractionStatus(int pawnId) + { + try + { + var pawn = PawnHelper.FindPawnById(pawnId); + if (pawn == null) return ApiResult.Fail($"Pawn {pawnId} not found."); + + var interactions = pawn.interactions; + if (interactions == null) return ApiResult.Fail("Pawn has no interaction tracker."); + + var traverse = Traverse.Create(interactions); + int lastInteractionTicks = traverse.Field("lastInteractionTime").GetValue(); + + bool tooRecently = interactions.InteractedTooRecentlyToInteract(); + int cooldownTicks = 0; + if (tooRecently) + { + // 120 ticks is a hardcoded cooldown in RimWorld's Pawn_InteractionsTracker.InteractedTooRecentlyToInteract() + cooldownTicks = (lastInteractionTicks + 120) - Find.TickManager.TicksGame; + if (cooldownTicks < 0) cooldownTicks = 0; + } + + var status = new PawnInteractionStatusDto + { + CanInteract = !tooRecently && pawn.Awake() && !pawn.Downed, + LastInteractionTicks = lastInteractionTicks, + CooldownTicks = cooldownTicks, + CooldownDays = GameTypesHelper.TicksToDays(cooldownTicks) + }; + + return ApiResult.Ok(status); + } + catch (Exception ex) + { + return ApiResult.Fail(ex.Message); + } + } + public ApiResult GetPawnInteractionLog(int pawnId, int limit = 50) + { + try + { + var pawn = PawnHelper.FindPawnById(pawnId); + if (pawn == null) + { + return ApiResult.Fail($"Pawn {pawnId} not found."); + } + + var playLog = Find.PlayLog; + + // Return a graceful empty DTO if the log is null (e.g., game just started) + if (playLog == null || playLog.AllEntries == null) + { + return ApiResult.Ok(new PawnInteractionLogDto + { + PawnId = pawnId, + Interactions = new List(), + Count = 0 + }); + } + + var logEntries = new List(); + + // Cache reflection flags outside the loop for maximum performance + var flags = BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public; + + var interactionType = typeof(PlayLogEntry_Interaction); + var initField = interactionType.GetField("initiator", flags); + var recField = interactionType.GetField("recipient", flags); + var intDefField = interactionType.GetField("intDef", flags); + + var singleType = typeof(PlayLogEntry_InteractionSinglePawn); + var sInitField = singleType.GetField("initiator", flags); + var sIntDefField = singleType.GetField("intDef", flags); + + // Reflection check: If field resolution fails, RimWorld internals might have changed. + if (initField == null || recField == null || intDefField == null || sInitField == null || sIntDefField == null) + { + Log.WarningOnce("RIMAPI: Failed to resolve PlayLogEntry_Interaction fields. Interaction logs may be incomplete.", 1948301); + } + + foreach (var entry in playLog.AllEntries) + { + // Fast native check: skip logs that don't involve this pawn to save CPU + if (!entry.Concerns(pawn)) continue; + + if (entry is PlayLogEntry_Interaction interaction) + { + Pawn initiator = initField?.GetValue(interaction) as Pawn; + Pawn recipient = recField?.GetValue(interaction) as Pawn; + + bool isInit = initiator != null && initiator.thingIDNumber == pawnId; + bool isRec = recipient != null && recipient.thingIDNumber == pawnId; + + if (isInit || isRec) + { + InteractionDef intDef = intDefField?.GetValue(interaction) as InteractionDef; + Thing povPawn = isInit ? initiator : recipient; + + logEntries.Add(new InteractionLogEntryDto + { + InitiatorId = initiator?.thingIDNumber ?? 0, + InitiatorName = initiator?.LabelShort ?? "Unknown", + RecipientId = recipient?.thingIDNumber ?? 0, + RecipientName = recipient?.LabelShort ?? "Unknown", + InteractionDefName = intDef?.defName, + InteractionLabel = intDef?.label, + Text = entry.ToGameStringFromPOV(povPawn), + Ticks = entry.Tick, + TimeAgo = (Find.TickManager.TicksGame - entry.Tick).ToStringTicksToPeriod() + }); + } + } + // Add back SinglePawn check for things like speeches or solo social interactions + else if (entry is PlayLogEntry_InteractionSinglePawn singleInteraction) + { + Pawn initiator = sInitField?.GetValue(singleInteraction) as Pawn; + + if (initiator != null && initiator.thingIDNumber == pawnId) + { + InteractionDef intDef = sIntDefField?.GetValue(singleInteraction) as InteractionDef; + + logEntries.Add(new InteractionLogEntryDto + { + InitiatorId = initiator.thingIDNumber, + InitiatorName = initiator.LabelShort, + RecipientId = 0, + RecipientName = "None", + InteractionDefName = intDef?.defName, + InteractionLabel = intDef?.label, + Text = entry.ToGameStringFromPOV(initiator), + Ticks = entry.Tick, + TimeAgo = (Find.TickManager.TicksGame - entry.Tick).ToStringTicksToPeriod() + }); + } + } + } + + var orderedInteractions = logEntries.OrderByDescending(e => e.Ticks).Take(limit).ToList(); + + var resultDto = new PawnInteractionLogDto + { + PawnId = pawnId, + Interactions = orderedInteractions, + Count = orderedInteractions.Count + }; + + return ApiResult.Ok(resultDto); + } + catch (Exception ex) + { + return ApiResult.Fail(ex.Message); + } + } + + public ApiResult GetPawnRelations(int pawnId) + { + try + { + var pawn = PawnHelper.FindPawnById(pawnId); + if (pawn == null) return ApiResult.Fail($"Pawn {pawnId} not found."); + + var result = new PawnRelationsDto + { + PawnId = pawn.thingIDNumber, + Relations = new List() + }; + + if (pawn.relations != null) + { + // 1. Cast a wider net to bypass cache limitations for Modded/DLC pets + var pawnsToCheck = new HashSet(); + + // Add explicitly cached family/social relations + pawnsToCheck.UnionWith(pawn.relations.RelatedPawns); + + // Add everyone on the current map to catch missing Familiars and standard Friends + if (pawn.Map != null) + { + foreach (var p in pawn.Map.mapPawns.AllPawns) + { + if (p != pawn) pawnsToCheck.Add(p); + } + } + + // 2. Evaluate relationships for everyone in our net + foreach (var otherPawn in pawnsToCheck) + { + // A: Explicit Relations (Brother, Ex-Lover, Bonded, Familiar) + foreach (PawnRelationDef relDef in pawn.GetRelations(otherPawn)) + { + result.Relations.Add(new PawnRelationEntryDto + { + OtherPawnId = otherPawn.thingIDNumber, + OtherPawnName = otherPawn.LabelShort, + RelationDefName = relDef.defName, + RelationLabel = relDef.GetGenderSpecificLabel(otherPawn) + }); + } + + // B: Implicit Social Situations (Friend / Rival) + // Animals don't have opinions of colonists in vanilla, so we ensure both are humanlike + if (pawn.RaceProps.Humanlike && otherPawn.RaceProps.Humanlike) + { + int opinion = pawn.relations.OpinionOf(otherPawn); + + if (opinion >= 20) + { + result.Relations.Add(new PawnRelationEntryDto + { + OtherPawnId = otherPawn.thingIDNumber, + OtherPawnName = otherPawn.LabelShort, + RelationDefName = "Friend", + RelationLabel = "Friend".Translate() + }); + } + else if (opinion <= -20) + { + result.Relations.Add(new PawnRelationEntryDto + { + OtherPawnId = otherPawn.thingIDNumber, + OtherPawnName = otherPawn.LabelShort, + RelationDefName = "Rival", + RelationLabel = "Rival".Translate() + }); + } + } + } + } + + return ApiResult.Ok(result); + } + catch (Exception ex) + { + return ApiResult.Fail(ex.Message); + } + } + + public ApiResult> GetPawnOpinions(int pawnId) + { + try + { + var pawn = PawnHelper.FindPawnById(pawnId); + if (pawn == null) return ApiResult>.Fail($"Pawn {pawnId} not found."); + + var opinions = new List(); + var allPawns = ColonistsHelper.GetColonistsList(); + + foreach (var otherPawn in allPawns) + { + if (otherPawn == pawn) continue; + + var opinionDto = new PawnOpinionDto + { + TargetPawnId = otherPawn.thingIDNumber, + TargetPawnName = otherPawn.LabelShort, + Opinion = pawn.relations.OpinionOf(otherPawn), + Breakdown = new List() + }; + + // Get thoughts breakdown + if (pawn.needs?.mood?.thoughts != null) + { + List socialThoughts = new List(); + pawn.needs.mood.thoughts.GetSocialThoughts(otherPawn, socialThoughts); + + foreach (var st in socialThoughts) + { + if (st is Thought_SituationalSocial ss) + { + if (ss.OtherPawn() == otherPawn) + { + opinionDto.Breakdown.Add(new OpinionBreakdownDto + { + ThoughtDefName = ss.def.defName, + Label = ss.LabelCap, + Score = ss.OpinionOffset() + }); + } + } + } + + // Memories + foreach (var memory in pawn.needs.mood.thoughts.memories.Memories) + { + if (memory is Thought_MemorySocial ms) + { + if (ms.OtherPawn() == otherPawn) + { + opinionDto.Breakdown.Add(new OpinionBreakdownDto + { + ThoughtDefName = ms.def.defName, + Label = ms.LabelCap, + Score = ms.OpinionOffset() + }); + } + } + } + } + + opinions.Add(opinionDto); + } + + return ApiResult>.Ok(opinions); + } + catch (Exception ex) + { + return ApiResult>.Fail(ex.Message); + } + } + + public ApiResult ForceInteraction(ForceInteractionRequestDto request) + { + try + { + var initiator = PawnHelper.FindPawnById(request.InitiatorId); + if (initiator == null) return ApiResult.Fail($"Initiator {request.InitiatorId} not found."); + + var recipient = PawnHelper.FindPawnById(request.RecipientId); + if (recipient == null) return ApiResult.Fail($"Recipient {request.RecipientId} not found."); + + var intDef = DefDatabase.GetNamed(request.InteractionDefName, false); + if (intDef == null) return ApiResult.Fail($"InteractionDef {request.InteractionDefName} not found."); + + bool success = initiator.interactions.TryInteractWith(recipient, intDef); + + if (success) + return ApiResult.Ok(); + else + return ApiResult.Fail("Interaction failed (pawns might be too far, unconscious, or busy)."); + } + catch (Exception ex) + { + return ApiResult.Fail(ex.Message); + } + } + + public ApiResult AddRelation(AddRelationRequestDto request) + { + try + { + var pawn1 = PawnHelper.FindPawnById(request.Pawn1Id); + if (pawn1 == null) return ApiResult.Fail($"Pawn {request.Pawn1Id} not found."); + + var pawn2 = PawnHelper.FindPawnById(request.Pawn2Id); + if (pawn2 == null) return ApiResult.Fail($"Pawn {request.Pawn2Id} not found."); + + var relDef = DefDatabase.GetNamed(request.RelationDefName, false); + if (relDef == null) return ApiResult.Fail($"PawnRelationDef {request.RelationDefName} not found."); + + pawn1.relations.AddDirectRelation(relDef, pawn2); + return ApiResult.Ok(); + } + catch (Exception ex) + { + return ApiResult.Fail(ex.Message); + } + } + + public ApiResult RemoveRelation(RemoveRelationRequestDto request) + { + try + { + var pawn1 = PawnHelper.FindPawnById(request.Pawn1Id); + if (pawn1 == null) return ApiResult.Fail($"Pawn {request.Pawn1Id} not found."); + + var pawn2 = PawnHelper.FindPawnById(request.Pawn2Id); + if (pawn2 == null) return ApiResult.Fail($"Pawn {request.Pawn2Id} not found."); + + var relDef = DefDatabase.GetNamed(request.RelationDefName, false); + if (relDef == null) return ApiResult.Fail($"PawnRelationDef {request.RelationDefName} not found."); + + if (pawn1.relations.DirectRelationExists(relDef, pawn2)) + { + pawn1.relations.RemoveDirectRelation(relDef, pawn2); + return ApiResult.Ok(); + } + else + { + return ApiResult.Fail("Relation does not exist."); + } + } + catch (Exception ex) + { + return ApiResult.Fail(ex.Message); + } + } + } +} diff --git a/docs/_api_macroses/controllers/PawnSocialController.yml b/docs/_api_macroses/controllers/PawnSocialController.yml new file mode 100644 index 0000000..cf26363 --- /dev/null +++ b/docs/_api_macroses/controllers/PawnSocialController.yml @@ -0,0 +1,245 @@ +title: '### :material-account-group: Pawn Social Controller' +desc: |- + The **Pawn Social Controller** manages interpersonal relationships, interactions, and opinions between pawns. + It allows for inspecting social logs, managing familial bonds, and forcing social interactions. + +/api/v1/game/defs/interactions: + desc: Retrieves a list of all valid InteractionDef names in the game (e.g., Chitchat, Insult, KindWords). + curl: |- + **Example:** + ```bash + curl --request GET \ + --url http://localhost:8765/api/v1/game/defs/interactions + ``` + request: '' + response: |- + **Response:** + ```json + { + "success": true, + "data": [ + { + "def_name": "Chitchat", + "label": "chitchat", + "description": "Casual conversation." + }, + { + "def_name": "Insult", + "label": "insult", + "description": "A direct insult." + } + ] + } + ``` + method: GET + +/api/v1/pawns/interactions: + desc: Gets the real-time interaction readiness of a specific pawn, including the current cooldown status. + curl: |- + **Example:** + ```bash + curl --request GET \ + --url 'http://localhost:8765/api/v1/pawns/interactions?pawn_id=779' + ``` + request: '' + response: |- + **Response:** + ```json + { + "success": true, + "data": { + "can_interact": true, + "last_interaction_ticks": 45000, + "cooldown_ticks": 0, + "cooldown_days": 0.0 + } + } + ``` + method: GET + +/api/v1/pawns/interactions/log: + desc: Retrieves the recent interaction history for a specific pawn. Supports an optional `limit` parameter (defaults to 50). + curl: |- + **Example:** + ```bash + curl --request GET \ + --url 'http://localhost:8765/api/v1/pawns/interactions/log?pawn_id=779&limit=5' + ``` + request: '' + response: |- + **Response:** + ```json + { + "success": true, + "data": { + "pawn_id": 779, + "interactions": [ + { + "initiator_id": 779, + "initiator_name": "Lumi", + "recipient_id": 780, + "recipient_name": "Blake", + "interaction_def_name": "Chitchat", + "interaction_label": "chitchat", + "text": "Lumi and Blake talked about meteorology.", + "ticks": 152000, + "time_ago": "2 hours ago" + } + ], + "count": 1 + } + } + ``` + method: GET + +/api/v1/pawns/relations: + desc: Retrieves all established familial and romantic relationships for this pawn (e.g., Parent, Child, Spouse, Lover). + curl: |- + **Example:** + ```bash + curl --request GET \ + --url 'http://localhost:8765/api/v1/pawns/relations?pawn_id=779' + ``` + request: '' + response: |- + **Response:** + ```json + { + "success": true, + "data": { + "pawn_id": 779, + "relations": [ + { + "other_pawn_id": 780, + "other_pawn_name": "Blake", + "relation_def_name": "Spouse", + "relation_label": "husband" + } + ] + } + } + ``` + method: GET + +/api/v1/pawns/opinions: + desc: Retrieves a list of this pawn's numerical opinion toward every other colonist, including a breakdown of thoughts and memories contributing to the score. + curl: |- + **Example:** + ```bash + curl --request GET \ + --url 'http://localhost:8765/api/v1/pawns/opinions?pawn_id=779' + ``` + request: '' + response: |- + **Response:** + ```json + { + "success": true, + "data": [ + { + "target_pawn_id": 780, + "target_pawn_name": "Blake", + "opinion": 45, + "breakdown": [ + { + "thought_def_name": "Pretty", + "label": "pretty", + "score": 20.0 + } + ] + } + ] + } + ``` + method: GET + +/api/v1/pawns/interactions/force: + desc: Forces a specific social interaction to occur immediately between two pawns. + curl: |- + **Example:** + ```bash + curl --request POST \ + --url http://localhost:8765/api/v1/pawns/interactions/force \ + --header 'content-type: application/json' \ + --data '{ + "initiator_id": 779, + "recipient_id": 780, + "interaction_def_name": "Chitchat" + }' + ``` + request: |- + **Request:** + ```json + { + "initiator_id": 779, + "recipient_id": 780, + "interaction_def_name": "Chitchat" + } + ``` + response: |- + **Response:** + ```json + { + "success": true, + "errors": [], + "warnings": [], + "timestamp": "2026-04-14T12:00:00.000Z" + } + ``` + method: POST + +/api/v1/pawns/relations/add: + desc: Instantly creates a permanent social bond (relation) between two pawns. + curl: |- + **Example:** + ```bash + curl --request POST \ + --url http://localhost:8765/api/v1/pawns/relations/add \ + --header 'content-type: application/json' \ + --data '{ + "pawn1_id": 779, + "pawn2_id": 780, + "relation_def_name": "Lover" + }' + ``` + request: |- + **Request:** + ```json + { + "pawn1_id": 779, + "pawn2_id": 780, + "relation_def_name": "Lover" + } + ``` + response: |- + **Response:** + ```json + { + "success": true, + "errors": [], + "warnings": [], + "timestamp": "2026-04-14T12:00:00.000Z" + } + ``` + method: POST + +/api/v1/pawns/relations/remove: + desc: Severs a specific social bond between two pawns. + curl: |- + **Example:** + ```bash + curl --request DELETE \ + --url 'http://localhost:8765/api/v1/pawns/relations/remove?pawn1_id=779&pawn2_id=780&relation_def_name=Lover' + ``` + request: '' + response: |- + **Response:** + ```json + { + "success": true, + "errors": [], + "warnings": [], + "timestamp": "2026-04-14T12:00:00.000Z" + } + ``` + method: DELETE diff --git a/docs/_api_macroses/controllers/ru/PawnSocialController.ru.yml b/docs/_api_macroses/controllers/ru/PawnSocialController.ru.yml new file mode 100644 index 0000000..1ca357b --- /dev/null +++ b/docs/_api_macroses/controllers/ru/PawnSocialController.ru.yml @@ -0,0 +1,245 @@ +title: '### :material-account-group: Социальное взаимодействие' +desc: |- + Контроллер **Pawn Social Controller** управляет межличностными отношениями, взаимодействиями и мнениями между пешками. + Он позволяет просматривать логи общения, управлять родственными связями и принудительно вызывать социальные взаимодействия. + +/api/v1/game/defs/interactions: + desc: Возвращает список всех доступных в игре типов социальных взаимодействий (например, Chitchat, Insult, KindWords). + curl: |- + **Пример:** + ```bash + curl --request GET \ + --url http://localhost:8765/api/v1/game/defs/interactions + ``` + request: '' + response: |- + **Ответ:** + ```json + { + "success": true, + "data": [ + { + "def_name": "Chitchat", + "label": "болтовня", + "description": "Обычный разговор." + }, + { + "def_name": "Insult", + "label": "оскорбление", + "description": "Прямое оскорбление." + } + ] + } + ``` + method: GET + +/api/v1/pawns/interactions: + desc: Получает готовность конкретной пешки к взаимодействию в реальном времени, включая текущий статус перезарядки. + curl: |- + **Пример:** + ```bash + curl --request GET \ + --url 'http://localhost:8765/api/v1/pawns/interactions?pawn_id=779' + ``` + request: '' + response: |- + **Ответ:** + ```json + { + "success": true, + "data": { + "can_interact": true, + "last_interaction_ticks": 45000, + "cooldown_ticks": 0, + "cooldown_days": 0.0 + } + } + ``` + method: GET + +/api/v1/pawns/interactions/log: + desc: Возвращает историю последних взаимодействий для конкретной пешки. Поддерживает необязательный параметр `limit` (по умолчанию 50). + curl: |- + **Пример:** + ```bash + curl --request GET \ + --url 'http://localhost:8765/api/v1/pawns/interactions/log?pawn_id=779&limit=5' + ``` + request: '' + response: |- + **Ответ:** + ```json + { + "success": true, + "data": { + "pawn_id": 779, + "interactions": [ + { + "initiator_id": 779, + "initiator_name": "Lumi", + "recipient_id": 780, + "recipient_name": "Blake", + "interaction_def_name": "Chitchat", + "interaction_label": "болтовня", + "text": "Люми и Блейк поговорили о метеорологии.", + "ticks": 152000, + "time_ago": "2 часа назад" + } + ], + "count": 1 + } + } + ``` + method: GET + +/api/v1/pawns/relations: + desc: Возвращает все установленные семейные и романтические отношения для этой пешки (например, родитель, ребенок, супруг, любовник). + curl: |- + **Пример:** + ```bash + curl --request GET \ + --url 'http://localhost:8765/api/v1/pawns/relations?pawn_id=779' + ``` + request: '' + response: |- + **Ответ:** + ```json + { + "success": true, + "data": { + "pawn_id": 779, + "relations": [ + { + "other_pawn_id": 780, + "other_pawn_name": "Blake", + "relation_def_name": "Spouse", + "relation_label": "муж" + } + ] + } + } + ``` + method: GET + +/api/v1/pawns/opinions: + desc: Возвращает список числовых мнений этой пешки обо всех остальных поселенцах, включая разбивку мыслей и воспоминаний, влияющих на оценку. + curl: |- + **Пример:** + ```bash + curl --request GET \ + --url 'http://localhost:8765/api/v1/pawns/opinions?pawn_id=779' + ``` + request: '' + response: |- + **Ответ:** + ```json + { + "success": true, + "data": [ + { + "target_pawn_id": 780, + "target_pawn_name": "Blake", + "opinion": 45, + "breakdown": [ + { + "thought_def_name": "Pretty", + "label": "красивый(ая)", + "score": 20.0 + } + ] + } + ] + } + ``` + method: GET + +/api/v1/pawns/interactions/force: + desc: Принудительно вызывает немедленное социальное взаимодействие между двумя пешками. + curl: |- + **Пример:** + ```bash + curl --request POST \ + --url http://localhost:8765/api/v1/pawns/interactions/force \ + --header 'content-type: application/json' \ + --data '{ + "initiator_id": 779, + "recipient_id": 780, + "interaction_def_name": "Chitchat" + }' + ``` + request: |- + **Запрос:** + ```json + { + "initiator_id": 779, + "recipient_id": 780, + "interaction_def_name": "Chitchat" + } + ``` + response: |- + **Ответ:** + ```json + { + "success": true, + "errors": [], + "warnings": [], + "timestamp": "2026-04-14T12:00:00.000Z" + } + ``` + method: POST + +/api/v1/pawns/relations/add: + desc: Мгновенно создает постоянную социальную связь (отношение) между двумя пешками. + curl: |- + **Пример:** + ```bash + curl --request POST \ + --url http://localhost:8765/api/v1/pawns/relations/add \ + --header 'content-type: application/json' \ + --data '{ + "pawn1_id": 779, + "pawn2_id": 780, + "relation_def_name": "Lover" + }' + ``` + request: |- + **Запрос:** + ```json + { + "pawn1_id": 779, + "pawn2_id": 780, + "relation_def_name": "Lover" + } + ``` + response: |- + **Ответ:** + ```json + { + "success": true, + "errors": [], + "warnings": [], + "timestamp": "2026-04-14T12:00:00.000Z" + } + ``` + method: POST + +/api/v1/pawns/relations/remove: + desc: Разрывает определенную социальную связь между двумя пешками. + curl: |- + **Пример:** + ```bash + curl --request DELETE \ + --url 'http://localhost:8765/api/v1/pawns/relations/remove?pawn1_id=779&pawn2_id=780&relation_def_name=Lover' + ``` + request: '' + response: |- + **Ответ:** + ```json + { + "success": true, + "errors": [], + "warnings": [], + "timestamp": "2026-04-14T12:00:00.000Z" + } + ``` + method: DELETE diff --git a/docs/api/pawns.md b/docs/api/pawns.md index 2fc6ac5..5c738c1 100644 --- a/docs/api/pawns.md +++ b/docs/api/pawns.md @@ -6,6 +6,7 @@ This section covers reading, spawning, editing, and controlling pawns, as well a {{ render_controllers([ 'PawnController', 'PawnInfoController', + 'PawnSocialController', 'PawnEditController', 'PawnJobController', 'PawnSpawnController', diff --git a/tests/bruno_api_collection/Pawn/Interactions/Add Relation.yml b/tests/bruno_api_collection/Pawn/Interactions/Add Relation.yml new file mode 100644 index 0000000..44d86e2 --- /dev/null +++ b/tests/bruno_api_collection/Pawn/Interactions/Add Relation.yml @@ -0,0 +1,23 @@ +info: + name: Add Relation + type: http + seq: 8 + +http: + method: POST + url: "{{baseURL}}/api/v1/pawns/relations/add" + body: + mode: json + json: |- + { + "pawn1_id": 779, + "pawn2_id": 780, + "relation_def_name": "Lover" + } + auth: inherit + +settings: + encodeUrl: true + timeout: 0 + followRedirects: true + maxRedirects: 5 diff --git a/tests/bruno_api_collection/Pawn/Interactions/Defs.yml b/tests/bruno_api_collection/Pawn/Interactions/Defs.yml new file mode 100644 index 0000000..884e118 --- /dev/null +++ b/tests/bruno_api_collection/Pawn/Interactions/Defs.yml @@ -0,0 +1,15 @@ +info: + name: Defs + type: http + seq: 1 + +http: + method: GET + url: "{{baseURL}}/api/v1/game/defs/interactions" + auth: inherit + +settings: + encodeUrl: true + timeout: 0 + followRedirects: true + maxRedirects: 5 diff --git a/tests/bruno_api_collection/Pawn/Interactions/Force Interaction.yml b/tests/bruno_api_collection/Pawn/Interactions/Force Interaction.yml new file mode 100644 index 0000000..3c253af --- /dev/null +++ b/tests/bruno_api_collection/Pawn/Interactions/Force Interaction.yml @@ -0,0 +1,23 @@ +info: + name: Force Interaction + type: http + seq: 7 + +http: + method: POST + url: "{{baseURL}}/api/v1/pawns/interactions/force" + body: + mode: json + json: |- + { + "initiator_id": 779, + "recipient_id": 780, + "interaction_def_name": "Chitchat" + } + auth: inherit + +settings: + encodeUrl: true + timeout: 0 + followRedirects: true + maxRedirects: 5 diff --git a/tests/bruno_api_collection/Pawn/Interactions/Pawn Interactions Log.yml b/tests/bruno_api_collection/Pawn/Interactions/Pawn Interactions Log.yml new file mode 100644 index 0000000..4be5d51 --- /dev/null +++ b/tests/bruno_api_collection/Pawn/Interactions/Pawn Interactions Log.yml @@ -0,0 +1,22 @@ +info: + name: Pawn Interactions Log + type: http + seq: 3 + +http: + method: GET + url: "{{baseURL}}/api/v1/pawns/interactions/log?pawn_id=779&limit=50" + params: + - name: pawn_id + value: "779" + type: query + - name: limit + value: "50" + type: query + auth: inherit + +settings: + encodeUrl: true + timeout: 0 + followRedirects: true + maxRedirects: 5 diff --git a/tests/bruno_api_collection/Pawn/Interactions/Pawn Interactions.yml b/tests/bruno_api_collection/Pawn/Interactions/Pawn Interactions.yml new file mode 100644 index 0000000..967baaa --- /dev/null +++ b/tests/bruno_api_collection/Pawn/Interactions/Pawn Interactions.yml @@ -0,0 +1,19 @@ +info: + name: Pawn Interactions + type: http + seq: 2 + +http: + method: GET + url: "{{baseURL}}/api/v1/pawns/interactions?pawn_id=779" + params: + - name: pawn_id + value: "779" + type: query + auth: inherit + +settings: + encodeUrl: true + timeout: 0 + followRedirects: true + maxRedirects: 5 diff --git a/tests/bruno_api_collection/Pawn/Interactions/Pawn Opinions.yml b/tests/bruno_api_collection/Pawn/Interactions/Pawn Opinions.yml new file mode 100644 index 0000000..c7dc9cf --- /dev/null +++ b/tests/bruno_api_collection/Pawn/Interactions/Pawn Opinions.yml @@ -0,0 +1,19 @@ +info: + name: Pawn Opinions + type: http + seq: 5 + +http: + method: GET + url: "{{baseURL}}/api/v1/pawns/opinions?pawn_id=776" + params: + - name: pawn_id + value: "776" + type: query + auth: inherit + +settings: + encodeUrl: true + timeout: 0 + followRedirects: true + maxRedirects: 5 diff --git a/tests/bruno_api_collection/Pawn/Interactions/Pawn Relations.yml b/tests/bruno_api_collection/Pawn/Interactions/Pawn Relations.yml new file mode 100644 index 0000000..cca41af --- /dev/null +++ b/tests/bruno_api_collection/Pawn/Interactions/Pawn Relations.yml @@ -0,0 +1,19 @@ +info: + name: Pawn Relations + type: http + seq: 4 + +http: + method: GET + url: "{{baseURL}}/api/v1/pawns/relations?pawn_id=776" + params: + - name: pawn_id + value: "776" + type: query + auth: inherit + +settings: + encodeUrl: true + timeout: 0 + followRedirects: true + maxRedirects: 5 diff --git a/tests/bruno_api_collection/Pawn/Interactions/Remove Relation.yml b/tests/bruno_api_collection/Pawn/Interactions/Remove Relation.yml new file mode 100644 index 0000000..ab72561 --- /dev/null +++ b/tests/bruno_api_collection/Pawn/Interactions/Remove Relation.yml @@ -0,0 +1,25 @@ +info: + name: Remove Relation + type: http + seq: 9 + +http: + method: DELETE + url: "{{baseURL}}/api/v1/pawns/relations/remove?pawn1_id=779&pawn2_id=780&relation_def_name=Lover" + params: + - name: pawn1_id + value: "779" + type: query + - name: pawn2_id + value: "780" + type: query + - name: relation_def_name + value: "Lover" + type: query + auth: inherit + +settings: + encodeUrl: true + timeout: 0 + followRedirects: true + maxRedirects: 5 diff --git a/tests/bruno_api_collection/Pawn/Interactions/folder.yml b/tests/bruno_api_collection/Pawn/Interactions/folder.yml new file mode 100644 index 0000000..a044d31 --- /dev/null +++ b/tests/bruno_api_collection/Pawn/Interactions/folder.yml @@ -0,0 +1,7 @@ +info: + name: Interactions + type: folder + seq: 15 + +request: + auth: inherit