Compare commits

...

5 Commits

Author SHA1 Message Date
comasqw
a20f2ba6a3 refactor Prototype class init 2024-11-19 17:12:42 +04:00
comasqw
c00de7fd09 refactor 2024-11-19 17:09:14 +04:00
Ed
ef6f23e4b8 Remove demiplan fun (#587)
* disable roomfills

* demiplan examination random disable
2024-11-16 17:37:06 +03:00
Ed
06308961ed Revert "Revert "Added personal signature system"" (#586)
* Revert "Revert "Added personal signature system (#382)" (#471)"

This reverts commit 9f93931057.

* Update pen.yml
2024-11-16 17:30:38 +03:00
Ed
7a0fb9fc15 fix wallmounted (#585) 2024-11-15 23:54:58 +03:00
40 changed files with 736 additions and 356 deletions

View File

@@ -55,7 +55,7 @@ public sealed partial class CP14DemiplaneSystem
return msg;
msg.AddMarkupOrThrow(
indexedLocation.Name is not null && _random.Prob(indexedLocation.ExamineProb)
indexedLocation.Name is not null/* && _random.Prob(indexedLocation.ExamineProb)*/
? Loc.GetString("cp14-demiplane-examine-title", ("location", Loc.GetString(indexedLocation.Name)))
: Loc.GetString("cp14-demiplane-examine-title-unknown"));
@@ -65,8 +65,8 @@ public sealed partial class CP14DemiplaneSystem
if (!_proto.TryIndex(modifier, out var indexedModifier))
continue;
if (!_random.Prob(indexedModifier.ExamineProb))
continue;
//if (!_random.Prob(indexedModifier.ExamineProb)) //temp disable
// continue;
if (indexedModifier.Name is null)
continue;

View File

@@ -0,0 +1,10 @@
using Robust.Shared.Audio;
namespace Content.Server._CP14.PersonalSignature;
[RegisterComponent]
public sealed partial class CP14PersonalSignatureComponent : Component
{
[DataField]
public SoundSpecifier? SignSound;
}

View File

@@ -0,0 +1,79 @@
using System.Diagnostics.CodeAnalysis;
using Content.Server.Mind;
using Content.Server.Paper;
using Content.Shared.Hands.Components;
using Content.Shared.Paper;
using Content.Shared.Verbs;
using Robust.Server.Audio;
using Robust.Shared.Audio;
using Robust.Shared.Player;
namespace Content.Server._CP14.PersonalSignature;
public sealed class CP14PersonalSignatureSystem : EntitySystem
{
[Dependency] private readonly AudioSystem _audio = default!;
[Dependency] private readonly MindSystem _mind = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<PaperComponent, GetVerbsEvent<AlternativeVerb>>(OnGetVerb);
}
private void OnGetVerb(Entity<PaperComponent> entity, ref GetVerbsEvent<AlternativeVerb> args)
{
if (!_mind.TryGetMind(args.User, out _, out var mind))
return;
if (mind.CharacterName is null)
return;
if (!CanSign(args.Using, out var signature))
return;
if (HasSign(entity, mind.CharacterName))
return;
args.Verbs.Add(new AlternativeVerb
{
Text = Loc.GetString("cp-sign-verb"),
Act = () =>
{
Sign(entity, mind.CharacterName, signature.SignSound);
},
});
}
private bool CanSign(EntityUid? item, [NotNullWhen(true)] out CP14PersonalSignatureComponent? personalSignature)
{
personalSignature = null;
return item is not null && TryComp(item, out personalSignature);
}
private bool HasSign(Entity<PaperComponent> entity, string sign)
{
foreach (var info in entity.Comp.StampedBy)
{
if (info.StampedName == sign)
return true;
}
return false;
}
private void Sign(Entity<PaperComponent> target, string name, SoundSpecifier? sound)
{
var info = new StampDisplayInfo
{
StampedName = name,
StampedColor = Color.Gray,
};
if (sound is not null)
_audio.PlayEntity(sound, Filter.Pvs(target), target, true);
target.Comp.StampedBy.Add(info);
}
}

View File

@@ -6,4 +6,9 @@ namespace Content.Shared._CP14.Wallmount;
[RegisterComponent, Access(typeof(CP14WallmountSystem))]
public sealed partial class CP14WallmountComponent : Component
{
[DataField]
public int AttachAttempts = 3;
[DataField]
public TimeSpan NextAttachTime = TimeSpan.Zero;
}

View File

@@ -1,7 +1,9 @@
using Content.Shared.Tag;
using Robust.Shared.Map;
using Robust.Shared.Map.Components;
using Robust.Shared.Network;
using Robust.Shared.Prototypes;
using Robust.Shared.Timing;
namespace Content.Shared._CP14.Wallmount;
@@ -10,6 +12,8 @@ public sealed class CP14WallmountSystem : EntitySystem
[Dependency] private readonly SharedTransformSystem _transform = default!;
[Dependency] private readonly SharedMapSystem _map = default!;
[Dependency] private readonly TagSystem _tag = default!;
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly INetManager _net = default!;
public static readonly ProtoId<TagPrototype>[] WallTags = {"Wall", "Window"};
@@ -17,18 +21,70 @@ public sealed class CP14WallmountSystem : EntitySystem
{
base.Initialize();
SubscribeLocalEvent<CP14WallmountComponent, MapInitEvent>(OnMapInit);
SubscribeLocalEvent<CP14WallmountedComponent, ComponentShutdown>(OnWallmountShutdown);
SubscribeLocalEvent<CP14WallmountedComponent, AnchorStateChangedEvent>(OnWallmountAnchorChanged);
}
private void OnMapInit(Entity<CP14WallmountComponent> ent, ref MapInitEvent args)
private void OnWallmountAnchorChanged(Entity<CP14WallmountedComponent> ent, ref AnchorStateChangedEvent args)
{
var grid = Transform(ent).GridUid;
if (!args.Anchored)
ClearWallmounts(ent);
}
private void OnWallmountShutdown(Entity<CP14WallmountedComponent> ent, ref ComponentShutdown args)
{
ClearWallmounts(ent);
}
public override void Update(float frameTime)
{
base.Update(frameTime);
var query = EntityQueryEnumerator<CP14WallmountComponent>();
while (query.MoveNext(out var uid, out var wallmount))
{
if (!wallmount.Initialized)
continue;
if (_timing.CurTime < wallmount.NextAttachTime)
continue;
if (wallmount.AttachAttempts <= 0)
{
if (_net.IsServer)
QueueDel(uid);
continue;
}
wallmount.NextAttachTime = _timing.CurTime + TimeSpan.FromSeconds(0.5f);
wallmount.AttachAttempts--;
if (TryAttachWallmount((uid, wallmount)))
{
RemComp<CP14WallmountComponent>(uid);
}
}
}
private void ClearWallmounts(Entity<CP14WallmountedComponent> ent)
{
foreach (var attached in ent.Comp.Attached)
{
QueueDel(attached);
}
ent.Comp.Attached.Clear();
}
private bool TryAttachWallmount(Entity<CP14WallmountComponent> wallmount)
{
var grid = Transform(wallmount).GridUid;
if (grid == null || !TryComp<MapGridComponent>(grid, out var gridComp))
return;
return false;
//Try found a wall in neighbour tile
var offset = Transform(ent).LocalRotation.ToWorldVec();
var targetPos = new EntityCoordinates(grid.Value,Transform(ent).LocalPosition - offset);
var offset = Transform(wallmount).LocalRotation.ToWorldVec().Normalized();
var targetPos = new EntityCoordinates(grid.Value, Transform(wallmount).LocalPosition - offset);
var anchored = _map.GetAnchoredEntities(grid.Value, gridComp, targetPos);
bool hasParent = false;
@@ -37,11 +93,15 @@ public sealed class CP14WallmountSystem : EntitySystem
if (!_tag.HasAnyTag(entt, WallTags))
continue;
_transform.SetParent(ent, entt);
EnsureComp<CP14WallmountedComponent>(entt, out var wallmounted);
if (!wallmounted.Attached.Contains(wallmount))
wallmounted.Attached.Add(wallmount);
hasParent = true;
break;
}
if (!hasParent)
QueueDel(ent);
return hasParent;
}
}

View File

@@ -0,0 +1,11 @@
namespace Content.Shared._CP14.Wallmount;
/// <summary>
/// Stores a list of all entities that are “attached” to this object. Destroying this object will destroy all attached entities
/// </summary>
[RegisterComponent, Access(typeof(CP14WallmountSystem))]
public sealed partial class CP14WallmountedComponent : Component
{
[DataField]
public HashSet<EntityUid> Attached = new();
}

View File

@@ -0,0 +1 @@
cp-sign-verb = Sign

View File

@@ -0,0 +1 @@
cp-sign-verb = Подписать

View File

@@ -17,6 +17,11 @@
- Write
- Pen
- CP14InkwellFittable
- type: CP14PersonalSignature
signSound:
collection: PaperScribbles
params:
variation: 0.1
# TODO: Чернильницу нужно доделать. Перо должно быть видно, пока оно внутри чернильницы. Предмет должен быть через ItemSlots, чтобы перо вставлять забирать можно было кликом. Нужно добавить прикольные звуки вставляния, выставляния. И добавить механ, который будет требовать периодически макать перо в чернильницу.
- type: entity

View File

@@ -286,17 +286,28 @@
minGroupSize: 1
maxGroupSize: 2
#- type: cp14DemiplaneModifier
# id: Ruins
# name: cp14-modifier-ruins
# reward: 0.35
# generationWeight: 2
# layers:
# - !type:OreDunGen
# entity: ##CP14DemiplanRuinsRoomSpawner
# count: 5
# minGroupSize: 1
# maxGroupSize: 1
- type: cp14DemiplaneModifier
id: Ruins
name: cp14-modifier-ruins
id: Loot
reward: 0.35
generationWeight: 2
layers:
- !type:OreDunGen
entity: CP14DemiplanRuinsRoomSpawner
count: 5
entity: CP14SpawnerExpeditionLootCommon
count: 15
minGroupSize: 1
maxGroupSize: 1
maxGroupSize: 3
- type: cp14DemiplaneModifier
id: EnemyXeno

View File

@@ -1,17 +1,17 @@
- type: Tag
id: CP14DemiplanRuins
- type: entity
id: CP14DemiplanRuinsRoomSpawner
categories: [ ForkFiltered ]
parent: BaseRoomMarker
name: Demiplan ruins room spawner
components:
- type: RoomFill
clearExisting: true
roomWhitelist:
tags:
- CP14DemiplanRuins
#- type: entity
# id: CP14DemiplanRuinsRoomSpawner
# categories: [ ForkFiltered ]
# parent: BaseRoomMarker
# name: Demiplan ruins room spawner
# components:
# - type: RoomFill
# clearExisting: true
# roomWhitelist:
# tags:
# - CP14DemiplanRuins
# 7x7
- type: dungeonRoom

View File

@@ -2,10 +2,10 @@
id: DemiplaneConnections
layers:
- !type:OreDunGen
entity: CP14DemiplanEnterRoomMarker
entity: CP14DemiplaneEntryPointMarker
count: 1
minGroupSize: 1
maxGroupSize: 1
minGroupSize: 4
maxGroupSize: 4
- !type:OreDunGen
entity: CP14DemiplanePassway
count: 2

View File

@@ -0,0 +1,5 @@
from .logger import get_logger, LogText
from .exceptions import *
from .parsers import YamlParser, FtlParser
from .prototype import Prototype, check_prototype_attrs
from .localization_helper import LocalizationHelper

View File

@@ -0,0 +1,6 @@
class ErrorWhileWritingToFile(Exception):
pass
class ErrorWhileReadingFromFile(Exception):
pass

View File

@@ -0,0 +1,180 @@
import json
import os
from . import Prototype, check_prototype_attrs, get_logger, LogText, ErrorWhileWritingToFile, ErrorWhileReadingFromFile
from .parsers import FtlParser, YamlParser, create_ftl
logger = get_logger(__name__)
SAVE_RESULT_TO = "entities.ftl"
YAML_PARSER_LAST_LAUNCH_RESULT_PATH = "last_launch_result/result.json"
class LocalizationHelper:
def __init__(self):
logger.debug("%s LocalizationHelper ", LogText.CLASS_INITIALIZATION)
@staticmethod
def _save_to_json(path: str, data: dict):
os.makedirs(os.path.dirname(path), exist_ok=True)
try:
logger.debug("%s: %s", LogText.SAVING_DATA_TO_FILE, path)
with open(path, "w", encoding="utf-8") as file:
json.dump(data, file, ensure_ascii=False, indent=4)
except Exception as e:
raise ErrorWhileWritingToFile(e)
@staticmethod
def _read_from_json(path: str) -> dict:
if os.path.exists(path):
try:
logger.debug("%s: %s", LogText.READING_DATA_FROM_FILE, path)
with open(path, encoding="utf-8") as file:
return json.load(file)
except Exception as e:
raise ErrorWhileReadingFromFile(e)
return {}
def _save_yaml_parser_last_launch_result(self, last_launch_result: dict[str, Prototype]):
logger.debug("%s %s", LogText.SAVING_LAST_LAUNCH_RESULT, YAML_PARSER_LAST_LAUNCH_RESULT_PATH)
prototypes_dict = {}
for prototype_id, prototype_obj in last_launch_result.items():
prototypes_dict[prototype_id] = prototype_obj.attrs_dict
self._save_to_json(YAML_PARSER_LAST_LAUNCH_RESULT_PATH, prototypes_dict)
def _read_prototypes_from_last_launch_result(self) -> dict[str, Prototype] | None:
if os.path.isfile(YAML_PARSER_LAST_LAUNCH_RESULT_PATH):
last_launch_result = self._read_from_json(YAML_PARSER_LAST_LAUNCH_RESULT_PATH)
last_launch_result_dict = {}
for prototype_id, prototype_attrs in last_launch_result.items():
last_launch_result_dict[prototype_id] = Prototype(prototype_attrs)
return last_launch_result_dict
return None
@staticmethod
def _update_prototype_if_attrs_has_been_changed(yaml_prototype_obj: Prototype, last_launch_prototype_obj: Prototype,
final_prototype_obj: Prototype):
if yaml_prototype_obj.attrs_dict != last_launch_prototype_obj.attrs_dict:
log_text = f"Has been updated from: {final_prototype_obj.attrs_dict}, to: "
for key, value in yaml_prototype_obj.attrs_dict.items():
if final_prototype_obj.attrs_dict[key] != value:
final_prototype_obj.set_attrs_dict_value(key, value)
log_text += f"{final_prototype_obj.attrs_dict}"
logger.debug(log_text)
return final_prototype_obj
def _merge_yaml_parser_prototypes_and_ftl_parser_prototypes(self, yaml_parser_prototypes: dict[str, Prototype],
ftl_parser_prototypes: dict[str, Prototype]) -> dict[str, Prototype]:
general_prototypes_dict = {}
last_launch_result = self._read_prototypes_from_last_launch_result()
for prototype_id, yaml_prototype_obj in yaml_parser_prototypes.items():
final_prototype_obj = yaml_prototype_obj
if prototype_id in ftl_parser_prototypes:
final_prototype_obj = ftl_parser_prototypes[prototype_id]
if last_launch_result and prototype_id in last_launch_result:
last_launch_prototype_obj = last_launch_result[prototype_id]
final_prototype_obj = self._update_prototype_if_attrs_has_been_changed(yaml_prototype_obj,
last_launch_prototype_obj,
final_prototype_obj)
general_prototypes_dict[prototype_id] = final_prototype_obj
return general_prototypes_dict
@staticmethod
def _set_parent_attrs(prototype_parent_id: str, prototype_obj: Prototype, parent_prototype_obj: Prototype):
for attr_name, attr_value in prototype_obj.attrs_dict.items():
if attr_value or attr_name in ("parent", "id"):
continue
parent_prototype_attr_value = parent_prototype_obj.attrs_dict.get(attr_name)
if parent_prototype_attr_value:
if attr_name == "name":
prototype_obj.name = f"{{ ent-{prototype_parent_id} }}"
elif attr_name == "description":
prototype_obj.description = f"{{ ent-{prototype_parent_id}.desc }}"
elif attr_name == "suffix":
prototype_obj.suffix = parent_prototype_attr_value
return prototype_obj
def _parent_checks(self, general_prototypes_dict: dict[str, Prototype]):
to_delete = []
for prototype_id, prototype_obj in general_prototypes_dict.items():
prototype_parent_id = prototype_obj.parent
if isinstance(prototype_parent_id, list):
continue
parent_prototype_obj = general_prototypes_dict.get(prototype_parent_id)
if parent_prototype_obj and check_prototype_attrs(parent_prototype_obj, False):
self._set_parent_attrs(prototype_parent_id, prototype_obj, parent_prototype_obj)
else:
if not check_prototype_attrs(prototype_obj, False):
to_delete.append(prototype_id)
for prototype_id in to_delete:
logger.debug("%s %s: %s", prototype_id, LogText.HAS_BEEN_DELETED, general_prototypes_dict[prototype_id])
del general_prototypes_dict[prototype_id]
return general_prototypes_dict
def _create_general_prototypes_dict(self, yaml_parser_prototypes: dict[str, Prototype],
ftl_parser_prototypes: dict[str, Prototype]) -> dict[str, Prototype]:
general_prototypes_dict = self._merge_yaml_parser_prototypes_and_ftl_parser_prototypes(yaml_parser_prototypes,
ftl_parser_prototypes)
self._save_yaml_parser_last_launch_result(yaml_parser_prototypes)
general_prototypes_dict = self._parent_checks(general_prototypes_dict)
return general_prototypes_dict
@staticmethod
def _create_result_ftl(general_prototypes_dict: dict[str, Prototype]) -> str:
result = ""
for prototype_obj in general_prototypes_dict.values():
result += create_ftl(prototype_obj)
return result
def _save_result(self, general_prototypes_dict: dict[str, Prototype]):
logger.debug("%s: %s", LogText.SAVING_FINAL_RESULT, SAVE_RESULT_TO)
result = self._create_result_ftl(general_prototypes_dict)
try:
with open(SAVE_RESULT_TO, "w", encoding="utf-8") as file:
file.write(result)
except Exception as e:
raise ErrorWhileWritingToFile(e)
@staticmethod
def _print_info(general_prototypes_dict):
logger.info("%s: %s prototypes", LogText.HAS_BEEN_PROCESSED, len(general_prototypes_dict))
logger.info("Logs in: 'logs/helper.log'")
def main(self, yaml_prototypes_path: str, ftl_prototypes_path: str):
try:
logger.debug("%s: %s", LogText.GETTING_PROTOTYPES_FROM_YAML, yaml_prototypes_path)
prototypes_list_parsed_from_yaml = YamlParser().get_prototypes(yaml_prototypes_path)
logger.debug("%s: %s", LogText.GETTING_PROTOTYPES_FROM_FTL, ftl_prototypes_path)
prototypes_list_parsed_from_ftl = FtlParser().get_prototypes(ftl_prototypes_path)
logger.debug(LogText.FORMING_FINAL_DICTIONARY)
general_prototypes_dict = self._create_general_prototypes_dict(prototypes_list_parsed_from_yaml,
prototypes_list_parsed_from_ftl)
self._save_result(general_prototypes_dict)
self._print_info(general_prototypes_dict)
except ErrorWhileWritingToFile as e:
logger.error("%s: %s", LogText.ERROR_WHILE_WRITING_DATA_TO_FILE, e, exc_info=True)
except ErrorWhileReadingFromFile as e:
logger.error("%s: %s", LogText.ERROR_WHILE_READING_DATA_FROM_FILE, e, exc_info=True)
except Exception as e:
logger.error("%s: %s", LogText.UNKNOWN_ERROR, e, exc_info=True)

View File

@@ -0,0 +1,43 @@
import logging
import os
LOG_FILE_PATH = os.path.join("logs", "helper.log")
os.makedirs(os.path.dirname(LOG_FILE_PATH), exist_ok=True)
file_handler = logging.FileHandler(LOG_FILE_PATH, mode='w', encoding="utf-8")
file_handler.setLevel(logging.DEBUG)
console_handler = logging.StreamHandler()
console_handler.setLevel(logging.INFO)
logging.basicConfig(
level=logging.DEBUG,
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
handlers=[
file_handler,
console_handler
]
)
def get_logger(name: str) -> logging.Logger:
return logging.getLogger(name)
class LogText:
CLASS_INITIALIZATION = "initializing"
GETTING_PROTOTYPES_FROM_YAML = "Getting prototypes from YAML files"
GETTING_PROTOTYPES_FROM_FTL = "Getting prototypes from FTL files"
FORMING_FINAL_DICTIONARY = "Forming the final prototypes dictionary"
SAVING_LAST_LAUNCH_RESULT = "Saving yaml_parser result to file"
READING_LAST_LAUNCH_RESULT = "Reading data from last launch"
SAVING_FINAL_RESULT = "Saving the final prototypes dictionary to FTL file"
SAVING_DATA_TO_FILE = "Saving data to file"
READING_DATA_FROM_FILE = "Reading data from file"
ERROR_WHILE_WRITING_DATA_TO_FILE = "Error while writing data to a file"
ERROR_WHILE_READING_DATA_FROM_FILE = "Error while writing data to a file"
UNKNOWN_ERROR = "An error occurred during execution"
HAS_BEEN_DELETED = "Has been deleted due to lack of relevant data"
HAS_BEEN_PROCESSED = "Has been processed"
FORMING_FTL_FOR_PROTOTYPE = "Forming FTL for Prototype"

View File

@@ -0,0 +1,3 @@
from .base_parser import BaseParser
from .yaml import YamlParser
from .fluent import FtlParser, create_ftl

View File

@@ -0,0 +1,28 @@
import os
from abc import ABC, abstractmethod
from LocalizationHelper.prototype import Prototype
class BaseParser(ABC):
@staticmethod
def _get_files_paths_in_dir(path: str) -> list[str]:
files_paths_lst = []
for dirpath, _, filenames in os.walk(path):
for filename in filenames:
file_path = f"{dirpath}\\{filename}"
files_paths_lst.append(file_path)
return files_paths_lst
@staticmethod
def _check_file_extension(path: str, extension: str) -> bool:
if path.endswith(extension):
return True
return False
@abstractmethod
def get_prototypes(self, prototypes_files_path: str) -> list[Prototype]:
pass

View File

@@ -0,0 +1,3 @@
from .ftl_reader import read_ftl
from .ftl_parser import FtlParser
from .ftl_writer import create_ftl

View File

@@ -0,0 +1,28 @@
from . import read_ftl
from LocalizationHelper import get_logger, LogText
from LocalizationHelper.prototype import Prototype
from LocalizationHelper.parsers import BaseParser
logger = get_logger(__name__)
class FtlParser(BaseParser):
def __init__(self):
logger.debug("%s FtlParser", LogText.CLASS_INITIALIZATION)
def get_prototypes(self, ftl_prototypes_path: str) -> dict[str, Prototype]:
prototypes = {}
ftl_prototypes_files_path = self._get_files_paths_in_dir(ftl_prototypes_path)
for prototype_file_path in ftl_prototypes_files_path:
if not self._check_file_extension(prototype_file_path, "ftl"):
continue
file_prototypes_dict = read_ftl(prototype_file_path)
for prototype_dict in file_prototypes_dict.values():
prototype_obj = Prototype(prototype_dict)
logger.debug("%s: %s", LogText.HAS_BEEN_PROCESSED, prototype_obj)
prototypes[prototype_obj.id] = prototype_obj
return prototypes

View File

@@ -1,14 +1,15 @@
def read_ftl(paths: tuple) -> dict:
"""
The function looks at each line of the ftl
file and determines by the indentation in the line whether
it is a new prototype or an attribute of an old one.
"""
from LocalizationHelper import get_logger, LogText
logger = get_logger(__name__)
def read_ftl(path: str) -> dict:
prototypes = {}
last_prototype = ""
path, error_log_path = paths
try:
logger.debug("%s: %s", LogText.READING_DATA_FROM_FILE, path)
with open(path, encoding="utf-8") as file:
for line in file.readlines():
if line.startswith("#") or line.startswith("\n"):
@@ -19,19 +20,19 @@ def read_ftl(paths: tuple) -> dict:
proto_id = proto_id.replace("ent-", "")
last_prototype = proto_id
prototypes[proto_id] = {
"name": proto_name.strip(),
"desc": None,
"suffix": None
}
"id": proto_id,
"name": proto_name.strip(),
"description": None,
"suffix": None
}
else:
if "desc" in line:
attr = "desc"
attr = "description"
elif "suffix" in line:
attr = "suffix"
prototypes[last_prototype][attr] = line.split(" = ")[-1].strip()
prototypes[last_prototype][attr] = line.split(" = ", 1)[1].strip()
except Exception as e:
with open(error_log_path, "a") as file:
file.write(f"FTL-ERROR:\nAn error occurred while reading a file {path}, error - {e}\n")
return prototypes
logger.error("%s: %s - %s", LogText.ERROR_WHILE_READING_DATA_FROM_FILE, path, e, exc_info=True)
else:
return prototypes

View File

@@ -0,0 +1,23 @@
from LocalizationHelper import get_logger, LogText
from LocalizationHelper.prototype import Prototype
logger = get_logger(__name__)
INDENT = " "
def create_ftl(prototype: Prototype) -> str:
logger.debug("%s: %s", LogText.FORMING_FTL_FOR_PROTOTYPE, prototype.attrs_dict)
ftl = ""
ftl += f"ent-{prototype.id} = {prototype.name}\n"
if prototype.description:
ftl += f"{INDENT}.desc = {prototype.description}\n"
if prototype.suffix:
ftl += f"{INDENT}.suffix = {prototype.suffix}\n"
ftl += "\n"
return ftl

View File

@@ -0,0 +1,2 @@
from .yaml_reader import read_yaml
from .yaml_parser import YamlParser

View File

@@ -0,0 +1,29 @@
from . import read_yaml
from LocalizationHelper import get_logger, LogText
from LocalizationHelper.prototype import Prototype, check_prototype_attrs
from LocalizationHelper.parsers import BaseParser
logger = get_logger(__name__)
class YamlParser(BaseParser):
def __init__(self):
logger.debug("%s YamlParser", LogText.CLASS_INITIALIZATION)
def get_prototypes(self, yaml_prototypes_path: str) -> dict[str, Prototype]:
prototypes = {}
yaml_prototypes_files_path = self._get_files_paths_in_dir(yaml_prototypes_path)
for prototype_file_path in yaml_prototypes_files_path:
if not self._check_file_extension(prototype_file_path, "yml"):
continue
file_prototypes_list: list[dict] = read_yaml(prototype_file_path)
if file_prototypes_list:
for prototype_dict in file_prototypes_list:
prototype_obj = Prototype(prototype_dict)
if check_prototype_attrs(prototype_obj):
logger.debug("%s: %s", LogText.HAS_BEEN_PROCESSED, prototype_obj)
prototypes[prototype_obj.id] = prototype_obj
return prototypes

View File

@@ -0,0 +1,31 @@
import yaml
from LocalizationHelper import get_logger, LogText
logger = get_logger(__name__)
# taken from - https://github.com/poeMota/map-converter-ss14/blob/master/src/Yaml/yaml.py
def unknown_tag_constructor(loader, tag_suffix, node):
if isinstance(node, yaml.ScalarNode):
value = loader.construct_scalar(node) # Node
elif isinstance(node, yaml.SequenceNode):
value = loader.construct_sequence(node) # List
elif isinstance(node, yaml.MappingNode):
value = loader.construct_mapping(node) # Dict
else:
raise TypeError(f"Unknown node type: {type(node)}")
return {tag_suffix: value}
yaml.add_multi_constructor('!', unknown_tag_constructor)
def read_yaml(path: str) -> list[dict]:
try:
logger.debug("%s: %s", LogText.READING_DATA_FROM_FILE, path)
with open(path, encoding="utf-8") as file:
data = yaml.full_load(file)
return data
except Exception as e:
logger.error("%s: %s - %s", LogText.ERROR_WHILE_READING_DATA_FROM_FILE, path, e, exc_info=True)

View File

@@ -0,0 +1,99 @@
class Prototype:
def __init__(self, prototype: dict):
self._name = prototype.get("name")
self._description = prototype.get("description")
self._parent = prototype.get("parent")
self._id = prototype.get("id")
self._suffix = prototype.get("suffix")
self._attrs_dict = {
"id": self._id,
"name": self._name,
"description": self._description,
"parent": self._parent,
"suffix": self._suffix
}
@property
def name(self):
return self._name
@name.setter
def name(self, new_name: str):
self._name = new_name
self._attrs_dict["name"] = new_name
@property
def description(self):
return self._description
@description.setter
def description(self, new_description: str):
self._description = new_description
self._attrs_dict["description"] = new_description
@property
def parent(self):
return self._parent
@parent.setter
def parent(self, new_parent: str):
self._parent = new_parent
self._attrs_dict["parent"] = new_parent
@property
def id(self):
return self._id
@id.setter
def id(self, new_id: str):
self._id = new_id
self._attrs_dict["id"] = new_id
@property
def suffix(self):
return self._suffix
@suffix.setter
def suffix(self, new_suffix: str):
self._suffix = new_suffix
self._attrs_dict["suffix"] = new_suffix
@property
def attrs_dict(self):
return self._attrs_dict
def set_attrs_dict_value(self, key, value):
self._attrs_dict[key] = value
if key == "name":
self._name = value
elif key == "description":
self._description = value
elif key == "id":
self._id = value
elif key == "parent":
self._parent = value
elif key == "suffix":
self._suffix = value
def __repr__(self):
return str(self._attrs_dict)
def check_prototype_attrs(prototype: Prototype, with_parent_check: bool = True) -> bool:
if prototype.name:
# if prototype.id == "CP14BaseWooden":
# print(prototype.name)
return True
elif prototype.description:
# if prototype.id == "CP14BaseWooden":
# print(prototype.description)
return True
elif prototype.suffix:
return True
# In some cases a parent can be a list (because of multiple parents),
# the game will not be able to handle such cases in ftl files.
elif with_parent_check and prototype.parent and not isinstance(prototype.parent, list):
return True
return False

View File

@@ -6,7 +6,7 @@ A script to help support game translation
## The right version of python: 3.10+
## Deployment
## Dependencies Install
in console
@@ -31,10 +31,18 @@ or just run
run.bat
```
## Demo
## Tests
in console
```bash
python run_tests.py
```
or just run
```bash
tests.bat
```
https://youtu.be/5HfDjLzhjA4
## License
Author: asqw: Discord - .asqw, GitHub - comasqw

View File

@@ -1,38 +0,0 @@
import os
import json
class BaseParser:
"""
BaseParser, contains the basic functions for the yml_parser module in the yml_parser package
and for the ftl_parser module in the ftl_parser package
"""
def __init__(self, paths: tuple):
self.path, self.errors_path = paths
def _get_files_paths(self) -> list:
"""
The method gets the path to the yml folder of localization prototypes/files, e.g. "ftl",
then with the help of os library goes through each file in
the folder and creates a path for it, e.g. "ftl/objects.ftl".
"""
files_paths_lst = []
for dirpath, _, filenames in os.walk(self.path):
for filename in filenames:
path = f"{dirpath}\\{filename}"
files_paths_lst.append(path)
return files_paths_lst
@staticmethod
def save_to_json(prototypes: dict, path: str) -> None:
with open(path, 'w') as json_file:
json.dump(prototypes, json_file, indent=4)
@staticmethod
def _check_file_extension(path: str, extension: str) -> bool:
if path.endswith(extension):
return True
return False

View File

@@ -1,8 +0,0 @@
{
"paths": {
"prototypes": "../../../Resources/Prototypes/_CP14/Entities",
"localization": "../../../Resources/Locale/ru-RU/_CP14/_PROTO/entities",
"error_log_path": "logs/errors.log",
"yml_parser_last_launch": "last_launch/yml_parser.json"
}
}

View File

@@ -1,17 +0,0 @@
import json
def create_ftl(key: str, prototype: dict) -> str:
ftl = ""
ftl += f"ent-{key} = {prototype["name"]}\n"
if prototype["desc"] is not None:
ftl += f" .desc = {prototype["desc"]}\n"
if prototype["suffix"] is not None:
ftl += f" .suffix = {prototype["suffix"]}\n"
ftl += "\n"
return ftl

View File

@@ -1 +0,0 @@
from .ftl_parser import FTLParser

View File

@@ -1,26 +0,0 @@
from fluent import ftl_reader
from base_parser import BaseParser
class FTLParser(BaseParser):
"""
The class inherits from the "BaseParser" class, parses ftl files of localization.
"""
def ftl_parser(self) -> dict:
"""
The function gets the path, then with the help of the os library
goes through each file,checks that the file extension is "ftl",
then reads it through the "ftl_reader" module of the "fluent" package.
"""
prototypes = {}
for path in self._get_files_paths():
if not self._check_file_extension(path, ".ftl"):
continue
file = ftl_reader.read_ftl((path, self.errors_path))
prototypes.update(file)
return prototypes

View File

@@ -1,136 +0,0 @@
import json
import os
from yml_parser import YMLParser
from ftl_parser import FTLParser
from fluent import ftl_writer
# Config constants
CONFIG_PATH = "config.json"
CONFIG_PATHS_KEY_NAME = "paths"
PROTOTYPES_PATH_IN_CONFIG = "prototypes"
FTL_PATH_IN_CONFIG = "localization"
ERRORS_LOG_PATH_IN_CONFIG = "error_log_path"
PARSED_PROTOTYPES_PATH_IN_LAST_LAUNCH = "yml_parser_last_launch"
LAST_LAUNCH_PROTOTYPES_DIR_NAME = "last_launch"
NAME_OF_FILE_TO_SAVE = "entities.ftl"
class LocalizationHelper:
if not os.path.isdir(LAST_LAUNCH_PROTOTYPES_DIR_NAME):
os.mkdir(LAST_LAUNCH_PROTOTYPES_DIR_NAME)
def __init__(self, config_path: str):
self._config = self._read_config(config_path)
self._prototypes_path, self._localization_path, self._errors_log_path, self._yml_parser_last_launch = self._get_paths()
self._clear_logs()
self.prototypes_dict_yml = YMLParser((self._prototypes_path, self._errors_log_path)).yml_parser()
self.prototypes_dict_ftl = FTLParser((self._localization_path, self._errors_log_path)).ftl_parser()
self._check_changed_attrs()
self.prototypes = {**self.prototypes_dict_yml, **self.prototypes_dict_ftl}
def _clear_logs(self):
with open(self._errors_log_path, "w") as file:
file.write("")
@staticmethod
def _read_config(config_path: str) -> dict:
with open(config_path, "r", encoding="utf-8") as file:
return json.load(file)
def _get_paths(self) -> tuple:
paths_dict = self._config[CONFIG_PATHS_KEY_NAME]
prototypes_path = paths_dict[PROTOTYPES_PATH_IN_CONFIG]
localization_path = paths_dict[FTL_PATH_IN_CONFIG]
errors_log_path = paths_dict[ERRORS_LOG_PATH_IN_CONFIG]
yml_parser_last_launch = paths_dict[PARSED_PROTOTYPES_PATH_IN_LAST_LAUNCH]
return prototypes_path, localization_path, errors_log_path, yml_parser_last_launch
def _check_changed_attrs(self):
"""
What error it fixes - without this function, changed attributes of prototypes that have not been changed in
localization files will simply not be added to the original ftl file, because the script first of all takes data
from localization files, if they exist, of course
The function gets the data received during the last run of the script, and checks if some attribute from
the last run has been changed,then simply replaces with this attribute the attribute
of the prototype received during parsing of localization files.
"""
if os.path.isfile(self._yml_parser_last_launch):
with open(self._yml_parser_last_launch, 'r', encoding='utf-8') as file:
last_launch_prototypes = json.load(file)
if last_launch_prototypes:
for prototype, last_launch_attrs in last_launch_prototypes.items():
if prototype in self.prototypes_dict_yml:
if prototype in self.prototypes_dict_ftl:
attrs = self.prototypes_dict_ftl[prototype]
proto_attrs_in_yml = self.prototypes_dict_yml[prototype]
for key, value in proto_attrs_in_yml.items():
if value != last_launch_attrs.get(key):
attrs[key] = value
self.prototypes_dict_ftl[prototype] = attrs
else:
if prototype in self.prototypes_dict_ftl:
del self.prototypes_dict_ftl[prototype]
@staticmethod
def _save_result(entities: str) -> None:
with open(NAME_OF_FILE_TO_SAVE, "w", encoding="utf-8") as file:
file.write(entities)
print(f"{NAME_OF_FILE_TO_SAVE} has been created\n")
@staticmethod
def _print_errors_log_info(errors_log_path: str, prototypes: dict) -> None:
with open(errors_log_path, "r") as file:
errors = file.read()
successful_count = len(prototypes) - errors.count("ERROR")
print(f"""Of the {len(prototypes)} prototypes, {successful_count} were successfully processed.
Errors can be found in {errors_log_path}
Number of errors during YML processing - {errors.count("YML-ERROR")}
Number of errors during FTL processing - {errors.count("FTL-ERROR")}
Number of errors during data extraction and creation of new FTL - {errors.count("RETRIEVING-ERROR")}""")
def main(self):
entities_ftl = ""
for prototype, prototype_attrs in self.prototypes.items():
try:
# This fragment is needed to restore some attributes after connecting the dictionary of
# prototypes parsed from ftl with the dictionary of prototypes parsed from yml.
if prototype in self.prototypes_dict_yml:
parent = self.prototypes_dict_yml[prototype]["parent"]
if parent and not isinstance(parent, list) and parent in self.prototypes_dict_yml:
if not prototype_attrs.get("name"):
prototype_attrs["name"] = f"{{ ent-{parent} }}"
if not prototype_attrs.get("desc"):
prototype_attrs["desc"] = f"{{ ent-{parent}.desc }}"
if not prototype_attrs.get("suffix"):
if self.prototypes_dict_yml[prototype].get("suffix"):
prototype_attrs["suffix"] = self.prototypes_dict_yml[prototype]["suffix"]
if any(prototype_attrs[attr] is not None for attr in ("name", "desc", "suffix")):
proto_ftl = ftl_writer.create_ftl(prototype, self.prototypes[prototype])
entities_ftl += proto_ftl
except Exception as e:
with open(self._errors_log_path, "a") as file:
print(prototype, prototype_attrs)
file.write(
f"RETRIEVING-ERROR:\nAn error occurred while retrieving data to be written to the file - {e}\n")
self._save_result(entities_ftl)
self._print_errors_log_info(self._errors_log_path, self.prototypes)
if __name__ == '__main__':
helper = LocalizationHelper(CONFIG_PATH)
helper.main()

View File

@@ -0,0 +1,11 @@
from LocalizationHelper import get_logger, LocalizationHelper
YAML_FILES_PATH = "../../../Resources/Prototypes/_CP14/Entities"
FTL_FILES_PATH = "../../../Resources/Locale/ru-RU/_CP14/_PROTO/entities"
if __name__ == '__main__':
logger = get_logger(__name__)
logger.info("Starting...")
helper = LocalizationHelper()
helper.main(YAML_FILES_PATH, FTL_FILES_PATH)

View File

@@ -1 +1 @@
PyYAML==6.0.1
PyYAML~=6.0.2

View File

@@ -1,3 +1,3 @@
@echo off
python localization_helper.py
python main.py
pause

View File

@@ -1 +0,0 @@
from .yml_parser import YMLParser

View File

@@ -1,76 +0,0 @@
import yaml
from base_parser import BaseParser
import re
class YMLParser(BaseParser):
"""
The class inherits from the "BaseParser" class, parses yml prototypes.
"""
@staticmethod
def _check_proto_attrs(prototype: dict) -> bool:
"""
The function checks that the prototype at least has some attribute from the "attrs_lst".
"""
attrs_lst = ["name", "description", "suffix"]
# In some cases a parent can be a list (because of multiple parents),
# the game will not be able to handle such cases in ftl files.
if not isinstance(prototype.get("parent"), list):
attrs_lst.append("parent")
return any(prototype.get(attr) is not None for attr in attrs_lst)
@staticmethod
def _get_proto_attrs(prototypes: dict, prototype: dict) -> None:
prototypes[prototype.get("id")] = {
"parent": prototype.get("parent"),
"name": prototype.get("name"),
"desc": prototype.get("description"),
"suffix": prototype.get("suffix")
}
def _load_proto(self, file, path) -> list[dict]:
content_str = file.read()
prototypes_lst = re.split(r"\n(?=- type:)", content_str)
prototypes = []
for proto in prototypes_lst:
try:
prototype_str = ""
for line in proto.splitlines():
if "components:" in line:
break
prototype_str += f"{line}\n"
prototype = yaml.safe_load(prototype_str)
if prototype is None:
continue
prototypes.append(prototype[0])
except Exception as e:
with open(self.errors_path, "a") as error_file:
error_file.write(
f"YML-ERROR:\nAn error occurred during prototype processing {path}, error - {e}\n")
return prototypes
def yml_parser(self) -> dict:
"""
The function gets the path, then with the help of the os library
goes through each file,checks that the file extension is "yml",
then processes the file using the "PyYaml" library
"""
prototypes = {}
for path in self._get_files_paths():
if not self._check_file_extension(path, ".yml"):
continue
with open(path, encoding="utf-8") as file:
content = self._load_proto(file, path)
if content is not None:
for prototype in content:
if self._check_proto_attrs(prototype):
self._get_proto_attrs(prototypes, prototype)
return prototypes