Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 27 additions & 13 deletions .github/mapchecker/mapchecker.py
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,7 @@

# Set up collectors.
violations: Dict[str, List[str]] = dict()
map_overrides: Dict[str, str] = dict()

# Check all maps for illegal prototypes.
for map_proto in map_proto_paths:
Expand Down Expand Up @@ -192,19 +193,26 @@
logger.debug(f"Map proto {map_proto} did not specify a map file location. Skipping.")
continue

# CHECKPOINT - If the map_name is blanket-whitelisted, skip it, but log a warning.
if map_name in whitelisted_maps:
logger.warning(f"Map '{map_name}' (from prototype '{map_proto}') was blanket-whitelisted. Skipping it.")
continue

if shipyard_override is not None:
# Log a warning, indicating the override and the normal group this shuttle belongs to, then set
# shipyard_group to the override.
logger.warning(f"Map '{map_name}' (from prototype '{map_proto}') is using mapchecker_group_override. "
f"This map will be treated as a '{shipyard_override}' shuttle. (Normally: "
f"'{shipyard_group}'))")
shipyard_group = shipyard_override

map_overrides[map_name] = shipyard_override

whitelisted = False
if map_name in whitelisted_maps:
logger.warning(f"Map '{map_name}' (from prototype '{map_proto}') was blanket-whitelisted. Skipping it.")
whitelisted = True
elif shipyard_override is not None and shipyard_override in whitelisted_maps:
logger.warning(f"Map '{map_name}' (checking as override '{shipyard_override}') was blanket-whitelisted. Skipping it.")
whitelisted = True

if whitelisted:
continue

logger.debug(f"Starting checks for '{map_name}' (Path: '{map_file_location}' | Shipyard: '{shipyard_group}')")

# Now construct a temporary list of all prototype ID's that are illegal for this map based on conditionals.
Expand Down Expand Up @@ -243,14 +251,20 @@
# PHASE 3: Filtering findings and reporting.
logger.debug(f"Violations aggregator before whitelist processing: {violations}")

# Filter out all prototypes that are whitelisted.
for key in whitelisted_protos.keys():
if violations.get(key) is None:
# Filter out all prototypes that are whitelisted.# Filter out all prototypes that are whitelisted.
for map_key in list(violations.keys()):
if len(violations[map_key]) == 0:
continue

for whitelisted_proto in whitelisted_protos[key]:
if whitelisted_proto in violations[key]:
violations[key].remove(whitelisted_proto)
if map_key in whitelisted_protos:
for whitelisted_proto in whitelisted_protos[map_key]:
if whitelisted_proto in violations[map_key]:
violations[map_key].remove(whitelisted_proto)
if map_key in map_overrides:
override_key = map_overrides[map_key]
if override_key in whitelisted_protos:
for whitelisted_proto in whitelisted_protos[override_key]:
if whitelisted_proto in violations[map_key]:
violations[map_key].remove(whitelisted_proto)

logger.debug(f"Violations aggregator after whitelist processing: {violations}")

Expand Down
7 changes: 5 additions & 2 deletions .github/workflows/nf-mapchecker.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,11 @@ on:
# Entity pathspecs - If any of these change (i.e. suffix changes etc), this check should run.
- "Resources/Prototypes/Entities/**/*.yml"
- "Resources/Prototypes/_NF/Entities/**/*.yml"
- "Resources/Prototypes/_Forge/Entities/**/*.yml"
- "Resources/Prototypes/Nyanotrasen/Entities/**/*.yml"
- "Resources/Prototypes/_DV/Entities/**/*.yml"
# Map pathspecs - If any maps are changed, this should run.
# - "Resources/Maps/**/*.yml"
- "Resources/Maps/**/*.yml"
# Also the mapchecker itself
- ".github/mapchecker/**"

Expand All @@ -34,4 +35,6 @@ jobs:
pip install -r .github/mapchecker/requirements.txt
- name: Run mapchecker
run: |
python3 .github/mapchecker/mapchecker.py
python3 .github/mapchecker/mapchecker.py --map_path "Resources/Prototypes/_NF/Maps/Outpost" "Resources/Prototypes/_NF/PointsOfInterest" "Resources/Prototypes/_NF/Shipyard"

# python3 .github/mapchecker/mapchecker.py --map_path "Resources/Prototypes/_NF/Maps/Outpost" "Resources/Prototypes/_NF/PointsOfInterest" "Resources/Prototypes/_NF/Shipyard" "Resources/Prototypes/_Forge/Maps/Outpost" "Resources/Prototypes/_Forge/PointsOfInterest" "Resources/Prototypes/_Forge/Shipyard"
46 changes: 42 additions & 4 deletions Content.IntegrationTests/Tests/_NF/ShipyardTests.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
using System.Collections.Generic; // Forge-Change
using System.Linq;
using Content.Server.Cargo.Systems;
using Content.Shared._NF.Shipyard; // Forge-Change
using Content.Shared._NF.Shipyard.Prototypes;
using Robust.Server.GameObjects;
using Robust.Shared.EntitySerialization.Systems;
Expand Down Expand Up @@ -76,6 +78,20 @@ public async Task NoShipyardShipArbitrage()
var protoManager = server.ResolveDependency<IPrototypeManager>();
var pricing = server.ResolveDependency<IEntitySystemManager>().GetEntitySystem<PricingSystem>();

// Forge-Change start
var factionMarkups = new Dictionary<ShipyardConsoleUiKey, float>()
{
{ ShipyardConsoleUiKey.Security, 1.35f },
{ ShipyardConsoleUiKey.Syndicate, 1.5f },
{ ShipyardConsoleUiKey.BlackMarket, 1.4f },
{ ShipyardConsoleUiKey.Mercenary, 1.3f },
{ ShipyardConsoleUiKey.Medical, 1.3f },
{ ShipyardConsoleUiKey.Scrap, 1.15f }
};

const float defaultMarkup = 1.2f;
// Forge-Change end

await server.WaitAssertion(() =>
{
Assert.Multiple(() =>
Expand All @@ -100,7 +116,6 @@ await server.WaitAssertion(() =>
Assert.That(mapLoaded, Is.True, $"Failed to load shuttle {vessel} ({vessel.ShuttlePath}): TryLoadGrid returned false.");
Assert.That(entManager.HasComponent<MapGridComponent>(shuttle.Value), Is.True);

// Grid failed to load, continue to the next map.
if (!mapLoaded)
continue;

Expand All @@ -109,10 +124,33 @@ await server.WaitAssertion(() =>
appraisePrice += price;
});

var idealMinPrice = appraisePrice * vessel.MinPriceMarkup;
// Forge-Change start
var shipyardConsole = vessel.Group;
float minMarkup;

if (factionMarkups.TryGetValue(shipyardConsole, out var factionMarkup))
{
minMarkup = factionMarkup;
}
else
{
minMarkup = defaultMarkup;
Console.WriteLine($"Faction '{shipyardConsole}' not found in markup dictionary. Using default markup: {defaultMarkup}");
}

var idealMinPrice = appraisePrice * minMarkup;
var markupPercent = (minMarkup - 1.0f) * 100;

Assert.That(vessel.Price, Is.AtLeast(idealMinPrice),
$"Arbitrage possible on {vessel.ID}. Minimal price should be {idealMinPrice}, {(vessel.MinPriceMarkup - 1.0f) * 100}% over the appraise price ({appraisePrice}).");
if (!vessel.MapcheckerException)
{
Assert.That(vessel.Price, Is.AtLeast(idealMinPrice),
$"Arbitrage possible on {vessel.ID}. " +
$"Minimal price should be {idealMinPrice:F2}, " +
$"{markupPercent:F1}% over the appraise price ({appraisePrice:F2}). " +
$"Faction: {shipyardConsole} " +
$"(Markup: {minMarkup:F2}, Prototype MinPriceMarkup: {vessel.MinPriceMarkup:F2})");
}
// Forge-Change end

map.DeleteMap(mapId);
}
Expand Down
3 changes: 3 additions & 0 deletions Content.Shared/_NF/Shipyard/Prototypes/VesselPrototype.cs
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,9 @@ public sealed class VesselPrototype : IPrototype, IInheritingPrototype
[DataField(required: true)]
public ShipyardConsoleUiKey Group = ShipyardConsoleUiKey.Shipyard;

[DataField]
public bool MapcheckerException = false;

/// <summary>
/// The purpose of the vessel. (e.g. Service, Cargo, Engineering etc.)
/// </summary>
Expand Down
1 change: 1 addition & 0 deletions Resources/Prototypes/_NF/Shipyard/Base/base.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
price: 50000
category: Medium
group: Shipyard
mapcheckerException: false
addComponents:
- type: IFF
color: '#FFFFFFFF'
Expand Down
160 changes: 160 additions & 0 deletions Tools/_Forge/Checks_fixers/Price_Fixer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
#!/usr/bin/env python3
"""
Обновляет цены шаттлов в прототипах на основе данных из логов
Округляет цены до ближайших сотен в большую сторону
Github: FireFoxPhoenix
"""

import os
import re
import sys
import argparse

def find_top_level_dir(start_directory: str, marker_file: str = MARKER_FILE) -> str:
current_dir = start_directory
while True:
try:
if marker_file in os.listdir(current_dir):
return current_dir
except (FileNotFoundError, PermissionError):
pass
parent_dir = os.path.dirname(current_dir)
if parent_dir == current_dir:
print(f"Failed to find {marker_file} starting from {start_directory}")
sys.exit(-1)
current_dir = parent_dir

def parse_log_file(log_path: str):
shuttle_prices = {}
if not os.path.exists(log_path):
print(f"Log file not found: {log_path}")
return shuttle_prices
try:
with open(log_path, 'r', encoding='utf-8') as f:
log_content = f.read()
except Exception as e:
print(f"Error reading log file: {e}")
return shuttle_prices
pattern = r'Arbitrage possible on (\w+?)\. Minimal price should be ([\d,]+)'
matches = re.findall(pattern, log_content)
for shuttle_name, price_str in matches:
price_str_clean = price_str.replace(',', '').replace('.', '')
try:
price = float(price_str_clean)
except ValueError:
print(f"Failed to parse price for {shuttle_name}: {price_str}")
continue
corrected_price = ((price + 99) // 100) * 100
shuttle_prices[shuttle_name] = int(corrected_price)
print(f"Found {len(shuttle_prices)} shuttles in log")
return shuttle_prices

def find_shuttle_prototype(root_dir: str, shuttle_name: str, prototypes_folder: str):
prototypes_path = os.path.join(root_dir, prototypes_folder)
if not os.path.exists(prototypes_path):
print(f"Prototypes folder not found: {prototypes_path}")
return None
for root, dirs, files in os.walk(prototypes_path):
for file in files:
if file.endswith('.yml'):
file_path = os.path.join(root, file)
try:
with open(file_path, 'r', encoding='utf-8') as f:
content = f.read()
if f"id: {shuttle_name}" in content or f"\n id: {shuttle_name}" in content:
return file_path
except Exception as e:
print(f"Error reading file {file_path}: {e}")
return None

def update_shuttle_price(file_path: str, shuttle_name: str, new_price: int, dry_run: bool = False):
try:
with open(file_path, 'r', encoding='utf-8') as f:
content = f.read()
except Exception as e:
print(f"Error reading file {file_path}: {e}")
return False
old_pattern = rf'(id:\s*{shuttle_name}\s*\n(?:[ \t-]*.*\n)*?[ \t-]*price:\s*)(\d+)'
match = re.search(old_pattern, content, re.MULTILINE)
if not match:
pattern = rf'(id:\s*{shuttle_name}.*?\n.*?price:\s*)(\d+)'
match = re.search(pattern, content, re.DOTALL)
if match:
old_price = match.group(2)
new_content = content[:match.start(2)] + str(new_price) + content[match.end(2):]
if content == new_content:
print(f" Price already correct: {old_price}")
return False
if dry_run:
print(f" [DRY RUN] Would update: {old_price} -> {new_price}")
return True
try:
with open(file_path, 'w', encoding='utf-8') as f:
f.write(new_content)
print(f" Updated: {old_price} -> {new_price}")
return True
except Exception as e:
print(f" Error writing file: {e}")
return False
else:
print(f" Could not find price for shuttle {shuttle_name} in file")
return False

def main():
parser = argparse.ArgumentParser(formatter_class=argparse.RawDescriptionHelpFormatter)
parser.add_argument(
'--log',
required=True,
help=f'Path to test log file'
)
parser.add_argument(
'--prototypes',
default='Resources/Prototypes/_Forge/Shipyard',
help=f'Shuttle prototypes folder'
)
parser.add_argument(
'--marker',
default='SpaceStation14.sln',
help=f'Marker file for finding project root'
)
parser.add_argument(
'--dry-run',
action='store_true',
help='Test mode - shows changes without applying them'
)
args = parser.parse_args()
start_directory = os.path.dirname(os.path.abspath(__file__))
root_dir = find_top_level_dir(start_directory, args.marker)
log_path = os.path.join(root_dir, args.log)
shuttle_prices = parse_log_file(log_path)
if not shuttle_prices:
print("No shuttle data found in log")
sys.exit(0)
print(f"\nProcessing {len(shuttle_prices)} shuttles...")
updated_count = 0
not_found_count = 0
already_correct_count = 0
for shuttle_name, new_price in shuttle_prices.items():
print(f"\nShuttle: {shuttle_name}")
print(f" New price: {new_price}")
prototype_file = find_shuttle_prototype(root_dir, shuttle_name, args.prototypes)
if prototype_file:
print(f" Found file: {os.path.relpath(prototype_file, root_dir)}")
updated = update_shuttle_price(prototype_file, shuttle_name, new_price, args.dry_run)
if updated:
updated_count += 1
else:
already_correct_count += 1
else:
print(f" Prototype file not found for shuttle {shuttle_name}")
not_found_count += 1

if __name__ == "__main__":
try:
main()
except KeyboardInterrupt:
print("\n\nInterrupted by user")
sys.exit(0)
except Exception as e:
print(f"\nCritical error: {e}")
sys.exit(1)
Loading