Freeciv-3.2: /fast/freeciv/nightly/S3_2/common/generate_packets.py Source File

Freeciv-3.2

Loading...
Searching...
No Matches
generate_packets.py
Go to the documentation of this file.
1#!/usr/bin/env python3
2
3#
4# Freeciv - Copyright (C) 2003 - Raimar Falke
5# This program is free software; you can redistribute it and/or modify
6# it under the terms of the GNU General Public License as published by
7# the Free Software Foundation; either version 2, or (at your option)
8# any later version.
9#
10# This program is distributed in the hope that it will be useful,
11# but WITHOUT ANY WARRANTY; without even the implied warranty of
12# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13# GNU General Public License for more details.
14#
15
16# This script runs under Python 3.5 and up. Please leave it so.
17# It might also run under older versions, but no such guarantees are made.
18
19import re
20import argparse
21import sys
22from pathlib import Path
23from contextlib import contextmanager, ExitStack
24from functools import partial
25from itertools import chain, combinations, takewhile, zip_longest
26from enum import Enum
27from abc import ABC, abstractmethod
28
29try:
30 from functools import cache
31except ImportError:
32 from functools import lru_cache
33 cache = lru_cache(None)
34 del lru_cache
35
36import typing
37T_co = typing.TypeVar("T_co", covariant = True)
38
39
40
41
42 def file_path(s: "str | Path") -> Path:
43 """Parse the given path and check basic validity."""
44 path = Path(s)
45
46 if path.is_reserved() or not path.name:
47 raise ValueError("not a valid file path: %r" % s)
48 if path.exists() and not path.is_file():
49 raise ValueError("not a file: %r" % s)
50
51 return path
52
53
54 class ScriptConfig:
55 """Contains configuration info for the script's execution, along with
56 functions closely tied to that configuration"""
57
58 @staticmethod
59 def get_argparser() -> argparse.ArgumentParser:
60 """Construct an argument parser for a packet generation script"""
61 parser = argparse.ArgumentParser(
62 description = "Generate packet-related code from packets.def",
63 add_help = False, # we'll add a help option explicitly
64 )
65
66 # Argument groups
67 # Note the order:
68 # We want the path arguments to show up *first* in the help text
69
70 paths = parser.add_argument_group(
71 "Input and output paths",
72 "The following parameters decide which files to read and write."
73 " Omitting an output path will not generate that file.",
74 )
75
76 script = parser.add_argument_group(
77 "Script configuration",
78 "The following parameters change how the script operates.",
79 )
80
81 output = parser.add_argument_group(
82 "Output configuration",
83 "The following parameters change the amount of output.",
84 )
85
86 protocol = parser.add_argument_group(
87 "Protocol configuration",
88 "The following parameters CHANGE the protocol."
89 " You have been warned.",
90 )
91
92 # Individual arguments
93 # Note the order:
94 # We want the path arguments to show up *last* in the usage summary
95
96 script.add_argument("-h", "--help", action = "help",
97 help = "show this help message and exit")
98
99 script.add_argument("-v", "--verbose", action = "store_true",
100 help = "enable log messages during code generation")
101
102 # When enabled: Only overwrite existing output files when they
103 # actually changed. This prevents make from rebuilding all dependents
104 # in cases where that wouldn't even be necessary.
105 script.add_argument("--lazy-overwrite", action = "store_true",
106 help = "only overwrite output files when their"
107 " contents actually changed")
108
109 output.add_argument("-s", "--gen-stats", action = "store_true",
110 help = "generate code reporting packet usage"
111 " statistics; call delta_stats_report to get these")
112
113 logs = output.add_mutually_exclusive_group()
114 logs.add_argument("-l", "--log-macro", default = "log_packet_detailed",
115 help = "use the given macro for generated log calls")
116 logs.add_argument("-L", "--no-logs", dest = "log_macro",
117 action = "store_const", const = None,
118 help = "disable generating log calls")
119
120 protocol.add_argument("-B", "--no-fold-bool",
121 dest = "fold_bool", action = "store_false",
122 help = "explicitly encode boolean values in the"
123 " packet body, rather than folding them into the"
124 " packet header")
125
126 output_path_args = (
127 # (dest, option, canonical path)
128 ("common_header_path", "--common-h", "common/packets_gen.h"),
129 ("common_impl_path", "--common-c", "common/packets_gen.c"),
130 ("client_header_path", "--client-h", "client/packhand_gen.h"),
131 ("client_impl_path", "--client-c", "client/packhand_gen.c"),
132 ("server_header_path", "--server-h", "server/hand_gen.h"),
133 ("server_impl_path", "--server-c", "server/hand_gen.c"),
134 )
135
136 for dest, option, canonical in output_path_args:
137 paths.add_argument(option, dest = dest, type = file_path,
138 help = "output path for %s" % canonical)
139
140 paths.add_argument("def_paths", metavar = "def_path",
141 nargs = "+", type = file_path,
142 help = "paths to your packets.def file")
143
144 return parser
145
146 def __init__(self, args: "typing.Sequence[str] | None" = None):
147 # type hints and docstrings for fields
148 # FIXME: Once we can use Python 3.6 features, turn these into
149 # (class-level) variable annotations
150 if typing.TYPE_CHECKING:
151 self.def_paths = [file_path("")]
152 """Paths to definition files, in load order"""
153 optional_path = (file_path(""), None)[int()]
154 self.common_header_path = optional_path
155 """Output path for the common header, or None if that should not
156 be generated"""
157 self.common_impl_path = optional_path
158 """Output path for the common implementation, or None if that
159 should not be generated"""
160 self.server_header_path = optional_path
161 """Output path for the server header, or None if that should not
162 be generated"""
163 self.server_impl_path = optional_path
164 """Output path for the server implementation, or None if that
165 should not be generated"""
166 self.client_header_path = optional_path
167 """Output path for the client header, or None if that should not
168 be generated"""
169 self.client_impl_path = optional_path
170 """Output path for the client implementation, or None if that
171 should not be generated"""
172
173 self.verbose = False
174 """Whether to enable verbose logging"""
175 self.lazy_overwrite = False
176 """Whether to lazily overwrite output files"""
177
178 self.gen_stats = False
179 """Whether to generate delta stats code"""
180 self.log_macro = str() or None
181 """The macro used for log calls, or None if no log calls should
182 be generated"""
183
184 self.fold_bool = True
185 """Whether to fold boolean fields into the packet header"""
186
187 __class__.get_argparser().parse_args(args, namespace = self)
188
189 def log_verbose(self, *args):
190 """Print the given arguments iff verbose logging is enabled"""
191 if self.verbose:
192 print(*args)
193
194 @property
195 def _root_path(self) -> "Path | None":
196 """Root Freeciv path, if we can find it."""
197 path = Path(__file__).absolute()
198 root = path.parent.parent
199 if path != root / "common" / "generate_packets.py":
200 self.log_verbose("Warning: couldn't find Freeciv root path")
201 return None
202 return root
203
204 def _relative_path(self, path: Path) -> Path:
205 """Find the relative path from the Freeciv root to the given path.
206 Return the path unmodified if it's outside the Freeciv root, or if
207 the Freeciv root could not be found."""
208 root = self._root_path
209 if root is not None:
210 try:
211 return path.absolute().relative_to(root)
212 except ValueError:
213 self.log_verbose("Warning: path %s outside of Freeciv root" % path)
214 return path
215
216 @property
217 def _script_path(self) -> Path:
218 """Relative path of the executed script. Under normal circumstances,
219 this will be common/generate_packets.py, but it may differ when this
220 module is imported from another script."""
221 return self._relative_path(Path(sys.argv[0]))
222
223 def _write_disclaimer(self, f: typing.TextIO):
224 f.write("""\
225 /**************************************************************************
226 * THIS FILE WAS GENERATED *
227 * Script: %-63s *
228""" % self._script_path)
229
230 for path in self.def_paths:
231 f.write("""\
232 * Input: %-63s *
233""" % self._relative_path(path))
234
235 f.write("""\
236 * DO NOT CHANGE THIS FILE *
237 **************************************************************************/
238
239""")
240
241 @contextmanager
242 def _wrap_header(self, file: typing.TextIO, header_name: str) -> typing.Iterator[None]:
243 """Add multiple inclusion protection to the given file"""
244 name = "FC__%s_H" % header_name.upper()
245 file.write("""\
246#ifndef {name}
247#define {name}
248
249""".format(name = name))
250
251 yield
252
253 file.write("""\
254
255#endif /* {name} */
256""".format(name = name))
257
258 @contextmanager
259 def _wrap_cplusplus(self, file: typing.TextIO) -> typing.Iterator[None]:
260 """Add code for `extern "C" {}` wrapping"""
261 file.write("""\
262#ifdef __cplusplus
263extern "C" {
264#endif /* __cplusplus */
265
266""")
267 yield
268 file.write("""\
269
270#ifdef __cplusplus
271}
272#endif /* __cplusplus */
273""")
274
275 @contextmanager
276 def open_write(self, path: "str | Path", wrap_header: "str | None" = None, cplusplus: bool = True) -> typing.Iterator[typing.TextIO]:
277 """Open a file for writing and write disclaimer.
278
279 If enabled, lazily overwrites the given file.
280 If wrap_header is given, add multiple inclusion protection; if
281 cplusplus is also given (default), also add code for `extern "C"`
282 wrapping."""
283 path = Path(path) # no-op if path is already a Path object
284 self.log_verbose("writing %s" % path)
285
286 with ExitStack() as stack:
287 if self.lazy_overwrite:
288 file = stack.enter_context(self.lazy_overwrite_open(path))
289 else:
290 file = stack.enter_context(path.open("w"))
291
292 self._write_disclaimer(file)
293
294 if wrap_header is not None:
295 stack.enter_context(self._wrap_header(file, wrap_header))
296 if cplusplus:
297 stack.enter_context(self._wrap_cplusplus(file))
298 yield file
299 self.log_verbose("done writing %s" % path)
300
301 @contextmanager
302 def lazy_overwrite_open(self, path: "str | Path", suffix: str = ".tmp") -> typing.Iterator[typing.TextIO]:
303 """Open a file for writing, but only actually overwrite it if the new
304 content differs from the old content.
305
306 This creates a temporary file by appending the given suffix to the given
307 file path. In the event of an error, this temporary file might remain in
308 the target file's directory."""
309
310 path = Path(path)
311 tmp_path = path.with_name(path.name + suffix)
312
313 # if tmp_path already exists, assume it's left over from a previous,
314 # failed run and can be overwritten without trouble
315 self.log_verbose("lazy: using %s" % tmp_path)
316 with tmp_path.open("w") as file:
317 yield file
318
319 if path.exists() and files_equal(tmp_path, path):
320 self.log_verbose("lazy: no change, deleting...")
321 tmp_path.unlink()
322 else:
323 self.log_verbose("lazy: content changed, replacing...")
324 tmp_path.replace(path)
325
326
327################### General helper functions and classes ###################
328
329def files_equal(path_a: "str | Path", path_b: "str | Path") -> bool:
330 """Return whether the contents of two text files are identical"""
331 with Path(path_a).open() as file_a, Path(path_b).open() as file_b:
332 return all(a == b for a, b in zip_longest(file_a, file_b))
333
334# Taken from https://docs.python.org/3.4/library/itertools.html#itertools-recipes
335def powerset(iterable: typing.Iterable[T_co]) -> "typing.Iterator[tuple[T_co, ...]]":
336 "powerset([1,2,3]) --> () (1,) (2,) (3,) (1,2) (1,3) (2,3) (1,2,3)"
337 s = list(iterable)
338 return chain.from_iterable(combinations(s, r) for r in range(len(s)+1))
339
340INSERT_PREFIX_PATTERN = re.compile(r"^(?!#|$)", re.MULTILINE)
341"""Matches the beginning of nonempty lines that are not preprocessor
342directives, i.e. don't start with a #"""
343
344def prefix(prefix: str, text: str) -> str:
345 """Prepend prefix to every line of text, except blank lines and those
346 starting with #"""
347 return INSERT_PREFIX_PATTERN.sub(prefix, text)
348
349
350class Location:
351 """Roughly represents a location in memory for the generated code;
352 outside of recursive field types like arrays, this will usually just be
353 a field of a packet, but it serves to concisely handle the recursion."""
354
355 _INDICES = "ijk"
356
357 def __init__(self, name: str, location: "str | None" = None, depth: int = 0):
358 self.name = name
359 """The name associated with this location; used in log messages."""
360 if location is None:
361 self.location = name
362 """The actual location as used in code"""
363 else:
364 self.location = location
365 self.depth = depth
366 """The array nesting depth of this location; used to determine index
367 variable names."""
368
369 def deeper(self, new_location: str) -> "Location":
370 """Return the given string as a new Location with the same name as
371 self and incremented depth"""
372 return type(self)(self.name, new_location, self.depth + 1)
373
374 @property
375 def index(self) -> str:
376 """The index name for the current depth"""
377 if self.depth > len(self._INDICES):
378 return self._INDICES[0] + str(self.depth) # i3, i4, i5...
379 return self._INDICES[self.depth]
380
381 @property
382 def sub(self) -> "Location":
383 """A location one level deeper with the current index subscript
384 added to the end.
385
386 `field` ~> `field[i]` ~> `field[i][j]` etc."""
387 return self.deeper("{self}[{self.index}]".format(self = self))
388
389 def __str__(self) -> str:
390 return self.location
391
392 def __repr__(self) -> str:
393 return "{self.__class__.__name__}({self.name!r}, {self.location!r}, {self.depth!r})".format(self = self)
394
395
396#################### Components of a packets definition ####################
397
398class FieldFlags:
399 """Information about flags of a given Field. Multiple Field objects can
400 share one FieldFlags instance, e.g. when defined on the same line."""
401
402 ADD_CAP_PATTERN = re.compile(r"^add-cap\‍(([^()]+)\‍)$")
403 """Matches an add-cap flag (optional capability)"""
404
405 REMOVE_CAP_PATTERN = re.compile(r"^remove-cap\‍(([^()]+)\‍)$")
406 """Matches a remove-cap flag (optional capability)"""
407
408 @classmethod
409 @cache
410 def parse(cls, flags_text: str) -> "FieldFlags":
411 """Parse a FieldFlags object from a comma-separated flag line"""
412 return cls(
413 stripped
414 for flag in flags_text.split(",")
415 for stripped in (flag.strip(),)
416 if stripped
417 )
418
419 is_key = False
420 """Whether the field is a key field"""
421
422 diff = False
423 """Whether the field should be deep-diffed for transmission"""
424
425 def __init__(self, flag_texts: typing.Iterable[str]):
426 self.add_caps = set()
427 """The capabilities required to enable the field"""
428
429 self.remove_caps = set()
430 """The capabilities that disable the field"""
431
432 for flag in flag_texts:
433 if flag == "key":
434 self.is_key = True
435 continue
436 if flag == "diff":
437 self.diff = True
438 continue
439 mo = __class__.ADD_CAP_PATTERN.fullmatch(flag)
440 if mo is not None:
441 self.add_caps.add(mo.group(1))
442 continue
443 mo = __class__.REMOVE_CAP_PATTERN.fullmatch(flag)
444 if mo is not None:
445 self.remove_caps.add(mo.group(1))
446 continue
447 raise ValueError("unrecognized flag in field declaration: %s" % flag)
448
449 contradictions = self.add_caps & self.remove_caps
450 if contradictions:
451 raise ValueError("cannot have same capabilities as both add-cap and remove-cap: %s" % ", ".join(contradictions))
452
453
454class SizeInfo:
455 """Information about size along one dimension of an array or other sized
456 field type. Contains both the declared / maximum size, and the actual
457 used size (if different)."""
458
459 ARRAY_SIZE_PATTERN = re.compile(r"^([^:]+)(?:\:([^:]+))?$")
460 """Matches an array size declaration (without the brackets)
461
462 Groups:
463 - the declared / maximum size
464 - the field name for the actual size (optional)"""
465
466 @classmethod
467 @cache
468 def parse(cls, size_text) -> "SizeInfo":
469 """Parse the given array size text (without brackets)"""
470 mo = cls.ARRAY_SIZE_PATTERN.fullmatch(size_text)
471 if mo is None:
472 raise ValueError("invalid array size declaration: [%s]" % size_text)
473 return cls(*mo.groups())
474
475 def __init__(self, declared: str, actual: "str | None"):
476 self.declared = declared
477 """Maximum size; used in declarations"""
478 self._actual = actual
479 """Name of the field to use for the actual size, or None if the
480 entire array should always be transmitted."""
481
482 @property
483 def constant(self) -> bool:
484 """Whether the actual size doesn't change"""
485 return self._actual is None
486
487 def actual_for(self, packet: str) -> str:
488 """Return a code expression representing the actual size for the
489 given packet"""
490 if self.constant:
491 return self.declared
492 return "%s->%s" % (packet, self._actual)
493
494 @property
495 def real(self) -> str:
496 """The number of elements to transmit. Either the same as the
497 declared size, or a field of `*real_packet`."""
498 return self.actual_for("real_packet")
499
500 @property
501 def old(self) -> str:
502 """The number of elements transmitted last time. Either the same as
503 the declared size, or a field of `*old`."""
504 return self.actual_for("old")
505
506 def receive_size_check(self, field_name: str) -> str:
507 """Generate a code snippet checking whether the received size is in
508 range when receiving a packet."""
509 if self.constant:
510 return ""
511 return """\
512if ({self.real} > {self.declared}) {{
513 RECEIVE_PACKET_FIELD_ERROR({field_name}, ": truncation array");
514}}
515""".format(self = self, field_name = field_name)
516
517 def __str__(self) -> str:
518 if self._actual is None:
519 return self.declared
520 return "%s:%s" % (self.declared, self._actual)
521
522 def __eq__(self, other) -> bool:
523 if not isinstance(other, __class__):
524 return NotImplemented
525 return (self.declared == other.declared
526 and self._actual == other._actual)
527
528 def __hash__(self) -> int:
529 return hash((__class__, self.declared, self._actual))
530
531
532class RawFieldType(ABC):
533 """Abstract base class (ABC) for classes representing types defined in a
534 packets definition file. These types may require the addition of a size
535 in order to be usable; see the array() method and the FieldType class."""
536
537 @abstractmethod
538 def array(self, size: SizeInfo) -> "FieldType":
539 """Add an array size to this field type, either to make a type which
540 needs a size fully useable, or to make an array type with self as
541 the element type."""
542 raise NotImplementedError
543
544 @abstractmethod
545 def __str__(self) -> str:
546 return super().__str__()
547
548 def __repr__(self) -> str:
549 return "<{self.__class__.__name__} {self}>".format(self = self)
550
551
552FieldTypeConstructor = typing.Callable[[str, str], RawFieldType]
553
554class TypeRegistry:
555 """Determines what Python class to use for field types based on their
556 dataio type and public type."""
557
558 TYPE_INFO_PATTERN = re.compile(r"^([^()]*)\‍(([^()]*)\‍)$")
559 """Matches a field type.
560
561 Groups:
562 - dataio type
563 - public type (aka struct type)"""
564
565 def __init__(self, fallback: FieldTypeConstructor):
566 # FIXME: Once we can use Python 3.6 features, turn these into
567 # (class-level) variable annotations
568 self.dataio_types = {} # type: dict[str, FieldTypeConstructor]
569 """Dictionary mapping dataio types to the constructor used for
570 field types with that dataio type.
571
572 This is the primary factor deciding what constructor to use for a
573 given field type."""
574 self.dataio_patterns = {} # type: dict[typing.Pattern[str], FieldTypeConstructor]
575 """Dictionary mapping RegEx patterns to the constructor used for
576 field types whose dataio type matches that pattern.
577
578 Matches are cached in self.dataio_types."""
579 self.public_types = {} # type: dict[str, FieldTypeConstructor]
580 """Like self.dataio_types, but for public types.
581
582 This is only checked if there are no matches in self.dataio_types
583 and self.dataio_patterns."""
584 self.public_patterns = {} # type: dict[typing.Pattern[str], FieldTypeConstructor]
585 """Like self.dataio_patterns, but for public types.
586
587 Matches are cached in self.public_types."""
588 self.fallback = fallback
589 """Fallback constructor used when there are no matches for either
590 dataio type or public type."""
591
592 def parse(self, type_text: str) -> RawFieldType:
593 """Parse a single field type"""
594 mo = __class__.TYPE_INFO_PATTERN.fullmatch(type_text)
595 if mo is None:
596 raise ValueError("malformed type or undefined alias: %r" % type_text)
597 return self(*mo.groups())
598
599 def __call__(self, dataio_type: str, public_type: str) -> RawFieldType:
600 try:
601 ctor = self.dataio_types[dataio_type]
602 except KeyError:
603 pass
604 else:
605 return ctor(dataio_type, public_type)
606
607 for pat, ctor in self.dataio_patterns.items():
608 mo = pat.fullmatch(dataio_type)
609 if mo is not None:
610 self.dataio_types[dataio_type] = ctor
611 return ctor(dataio_type, public_type)
612
613 self.dataio_types[dataio_type] = self._by_public
614 return self._by_public(dataio_type, public_type)
615
616 def _by_public(self, dataio_type: str, public_type: str) -> RawFieldType:
617 try:
618 ctor = self.public_types[public_type]
619 except KeyError:
620 pass
621 else:
622 return ctor(dataio_type, public_type)
623
624 for pat, ctor in self.public_patterns.items():
625 mo = pat.fullmatch(public_type)
626 if mo is not None:
627 self.public_types[public_type] = ctor
628 return ctor(dataio_type, public_type)
629
630 self.public_types[public_type] = self.fallback
631 return self.fallback(dataio_type, public_type)
632
633
634class NeedSizeType(RawFieldType):
635 """Helper class for field types that require a size to be usable."""
636
637 def __init__(self, dataio_type: str, public_type: str, cls: typing.Callable[[str, str, SizeInfo], "FieldType"]):
638 self.dataio_type = dataio_type
639 """The dataio type passed to self.cls"""
640 self.public_type = public_type
641 """The public type passed to self.cls"""
642 self.cls = cls
643 """The field type constructed when adding a size to this type"""
644
645 def array(self, size: SizeInfo) -> "FieldType":
646 """Add an array size to make a useable type."""
647 return self.cls(self.dataio_type, self.public_type, size)
648
649 def __str__(self) -> str:
650 return "{self.dataio_type}({self.public_type})".format(self = self)
651
652
653class FieldType(RawFieldType):
654 """Abstract base class (ABC) for classes representing type information
655 usable for fields of a packet"""
656
657 foldable = False
658 """Whether a field of this type can be folded into the packet header"""
659
660 @cache
661 def array(self, size: SizeInfo) -> "FieldType":
662 """Construct a FieldType for an array with element type self and the
663 given size"""
664 return ArrayType(self, size)
665
666 @abstractmethod
667 def get_code_declaration(self, location: Location) -> str:
668 """Generate a code snippet declaring a field with this type in a
669 packet struct."""
670 raise NotImplementedError
671
672 @abstractmethod
673 def get_code_handle_param(self, location: Location) -> str:
674 """Generate a code fragment declaring a parameter with this type for
675 a handle function.
676
677 See also self.get_code_handle_arg()"""
678 raise NotImplementedError
679
680 def get_code_handle_arg(self, location: Location) -> str:
681 """Generate a code fragment passing an argument with this type to a
682 handle function.
683
684 See also self.get_code_handle_param()"""
685 return str(location)
686
687 @abstractmethod
688 def get_code_fill(self, location: Location) -> str:
689 """Generate a code snippet moving a value of this type from dsend
690 arguments into a packet struct."""
691 raise NotImplementedError
692
693 @abstractmethod
694 def get_code_hash(self, location: Location) -> str:
695 """Generate a code snippet factoring a field of this type into a
696 hash computation's `result`."""
697 raise NotImplementedError
698
699 @abstractmethod
700 def get_code_cmp(self, location: Location) -> str:
701 """Generate a code snippet comparing a field of this type between
702 the `old` and `real_packet` and setting `differ` accordingly."""
703 raise NotImplementedError
704
705 @abstractmethod
706 def get_code_put(self, location: Location, deep_diff: bool = False) -> str:
707 """Generate a code snippet writing a field of this type to the
708 dataio stream."""
709 raise NotImplementedError
710
711 @abstractmethod
712 def get_code_get(self, location: Location, deep_diff: bool = False) -> str:
713 """Generate a code snippet reading a field of this type from the
714 dataio stream."""
715 raise NotImplementedError
716
717
718class BasicType(FieldType):
719 """Type information for a field without any specialized treatment"""
720
721 def __init__(self, dataio_type: str, public_type: str):
722 self.dataio_type = dataio_type
723 """How fields of this type are transmitted over network"""
724 self.public_type = public_type
725 """How fields of this type are represented in C code"""
726
727 def get_code_declaration(self, location: Location) -> str:
728 return """\
729{self.public_type} {location};
730""".format(self = self, location = location)
731
732 def get_code_handle_param(self, location: Location) -> str:
733 return "{self.public_type} {location}".format(self = self, location = location)
734
735 def get_code_fill(self, location: Location) -> str:
736 return """\
737real_packet->{location} = {location};
738""".format(location = location)
739
740 def get_code_hash(self, location: Location) -> str:
741 raise ValueError("hash not supported for type %s in field %s" % (self, location.name))
742
743 def get_code_cmp(self, location: Location) -> str:
744 return """\
745differ = (old->{location} != real_packet->{location});
746""".format(self = self, location = location)
747
748 def get_code_put(self, location: Location, deep_diff: bool = False) -> str:
749 return """\
750e |= DIO_PUT({self.dataio_type}, &dout, &field_addr, real_packet->{location});
751""".format(self = self, location = location)
752
753 def get_code_get(self, location: Location, deep_diff: bool = False) -> str:
754 return """\
755if (!DIO_GET({self.dataio_type}, &din, &field_addr, &real_packet->{location})) {{
756 RECEIVE_PACKET_FIELD_ERROR({location.name});
757}}
758""".format(self = self, location = location)
759
760 def __str__(self) -> str:
761 return "{self.dataio_type}({self.public_type})".format(self = self)
762
763DEFAULT_REGISTRY = TypeRegistry(BasicType)
764"""The default type registry used by a PacketsDefinition when no other
765registry is given."""
766
767
768class IntType(BasicType):
769 """Type information for an integer field"""
770
771 TYPE_PATTERN = re.compile(r"^[su]int\d+$")
772 """Matches an int dataio type"""
773
774 @typing.overload
775 def __init__(self, dataio_info: str, public_type: str): ...
776 @typing.overload
777 def __init__(self, dataio_info: "re.Match[str]", public_type: str): ...
778 def __init__(self, dataio_info: "str | re.Match[str]", public_type: str):
779 if isinstance(dataio_info, str):
780 mo = self.TYPE_PATTERN.fullmatch(dataio_info)
781 if mo is None:
782 raise ValueError("not a valid int type")
783 dataio_info = mo
784 dataio_type = dataio_info.group(0)
785
786 super().__init__(dataio_type, public_type)
787
788 def get_code_hash(self, location: Location) -> str:
789 return """\
790result += key->%s;
791""" % location
792
793 def get_code_get(self, location: Location, deep_diff: bool = False) -> str:
794 if self.public_type in ("int", "bool"):
795 # read directly
796 return super().get_code_get(location, deep_diff)
797 # read indirectly to make sure coercions between different integer
798 # types happen correctly
799 return """\
800{{
801 int readin;
802
803 if (!DIO_GET({self.dataio_type}, &din, &field_addr, &readin)) {{
804 RECEIVE_PACKET_FIELD_ERROR({location.name});
805 }}
806 real_packet->{location} = readin;
807}}
808""".format(self = self, location = location)
809
810DEFAULT_REGISTRY.dataio_patterns[IntType.TYPE_PATTERN] = IntType
811
812
813class BoolType(IntType):
814 """Type information for a boolean field"""
815
816 TYPE_PATTERN = re.compile(r"^bool\d*$")
817 """Matches a bool dataio type"""
818
819 foldable = True
820
821 @typing.overload
822 def __init__(self, dataio_info: str, public_type: str): ...
823 @typing.overload
824 def __init__(self, dataio_info: "re.Match[str]", public_type: str): ...
825 def __init__(self, dataio_info: "str | re.Match[str]", public_type: str):
826 if isinstance(dataio_info, str):
827 mo = self.TYPE_PATTERN.fullmatch(dataio_info)
828 if mo is None:
829 raise ValueError("not a valid bool type")
830 dataio_info = mo
831
832 if public_type != "bool":
833 raise ValueError("bool dataio type with non-bool public type: %r" % public_type)
834
835 super().__init__(dataio_info, public_type)
836
837DEFAULT_REGISTRY.dataio_patterns[BoolType.TYPE_PATTERN] = BoolType
838
839
840class FloatType(BasicType):
841 """Type information for a float field"""
842
843 TYPE_PATTERN = re.compile(r"^([su]float)(\d+)?$")
844 """Matches a float dataio type
845
846 Note: Will also match float types without a float factor to avoid
847 falling back to the default; in this case, the second capturing group
848 will not match.
849
850 Groups:
851 - non-numeric dataio type
852 - numeric float factor"""
853
854 @typing.overload
855 def __init__(self, dataio_info: str, public_type: str): ...
856 @typing.overload
857 def __init__(self, dataio_info: "re.Match[str]", public_type: str): ...
858 def __init__(self, dataio_info: "str | re.Match[str]", public_type: str):
859 if isinstance(dataio_info, str):
860 mo = self.TYPE_PATTERN.fullmatch(dataio_info)
861 if mo is None:
862 raise ValueError("not a valid float type")
863 dataio_info = mo
864 dataio_type, float_factor = dataio_info.groups()
865 if float_factor is None:
866 raise ValueError("float type without float factor: %r" % dataio_info.string)
867
868 if public_type != "float":
869 raise ValueError("float dataio type with non-float public type: %r" % public_type)
870
871 super().__init__(dataio_type, public_type)
872 self.float_factor = int(float_factor)
873 """Granularity (fixed-point factor) used to transmit this type in an
874 integer"""
875
876 def get_code_put(self, location: Location, deep_diff: bool = False) -> str:
877 return """\
878e |= DIO_PUT({self.dataio_type}, &dout, &field_addr, real_packet->{location}, {self.float_factor:d});
879""".format(self = self, location = location)
880
881 def get_code_get(self, location: Location, deep_diff: bool = False) -> str:
882 return """\
883if (!DIO_GET({self.dataio_type}, &din, &field_addr, &real_packet->{location}, {self.float_factor:d})) {{
884 RECEIVE_PACKET_FIELD_ERROR({location.name});
885}}
886""".format(self = self, location = location)
887
888 def __str__(self) -> str:
889 return "{self.dataio_type}{self.float_factor:d}({self.public_type})".format(self = self)
890
891DEFAULT_REGISTRY.dataio_patterns[FloatType.TYPE_PATTERN] = FloatType
892
893
894class BitvectorType(BasicType):
895 """Type information for a bitvector field"""
896
897 def __init__(self, dataio_type: str, public_type: str):
898 if dataio_type != "bitvector":
899 raise ValueError("not a valid bitvector type")
900
901 super().__init__(dataio_type, public_type)
902
903 def get_code_cmp(self, location: Location) -> str:
904 return """\
905differ = !BV_ARE_EQUAL(old->{location}, real_packet->{location});
906""".format(self = self, location = location)
907
908 def get_code_put(self, location: Location, deep_diff: bool = False) -> str:
909 return """\
910e |= DIO_BV_PUT(&dout, &field_addr, packet->{location});
911""".format(location = location)
912
913 def get_code_get(self, location: Location, deep_diff: bool = False) -> str:
914 return """\
915if (!DIO_BV_GET(&din, &field_addr, real_packet->{location})) {{
916 RECEIVE_PACKET_FIELD_ERROR({location.name});
917}}
918""".format(self = self, location = location)
919
920DEFAULT_REGISTRY.dataio_types["bitvector"] = BitvectorType
921
922
923class StructType(BasicType):
924 """Type information for a field of some general struct type"""
925
926 TYPE_PATTERN = re.compile(r"^struct \w+$")
927 """Matches a struct public type"""
928
929 @typing.overload
930 def __init__(self, dataio_type: str, public_info: str): ...
931 @typing.overload
932 def __init__(self, dataio_type: str, public_info: "re.Match[str]"): ...
933 def __init__(self, dataio_type: str, public_info: "str | re.Match[str]"):
934 if isinstance(public_info, str):
935 mo = self.TYPE_PATTERN.fullmatch(public_info)
936 if mo is None:
937 raise ValueError("not a valid struct type")
938 public_info = mo
939 public_type = public_info.group(0)
940
941 super().__init__(dataio_type, public_type)
942
943 def get_code_handle_param(self, location: Location) -> str:
944 if not location.depth:
945 # top level: pass by-reference
946 return "const " + super().get_code_handle_param(location.deeper("*%s" % location))
947 return super().get_code_handle_param(location)
948
949 def get_code_handle_arg(self, location: Location) -> str:
950 if not location.depth:
951 # top level: pass by-reference
952 return super().get_code_handle_arg(location.deeper("&%s" % location))
953 return super().get_code_handle_arg(location)
954
955 def get_code_cmp(self, location: Location) -> str:
956 return """\
957differ = !are_{self.dataio_type}s_equal(&old->{location}, &real_packet->{location});
958""".format(self = self, location = location)
959
960 def get_code_put(self, location: Location, deep_diff: bool = False) -> str:
961 return """\
962e |= DIO_PUT({self.dataio_type}, &dout, &field_addr, &real_packet->{location});
963""".format(self = self, location = location)
964
965DEFAULT_REGISTRY.public_patterns[StructType.TYPE_PATTERN] = StructType
966
967
968class CmParameterType(StructType):
969 """Type information for a worklist field"""
970
971 def __init__(self, dataio_type: str, public_type: str):
972 if dataio_type != "cm_parameter":
973 raise ValueError("not a valid cm_parameter type")
974
975 if public_type != "struct cm_parameter":
976 raise ValueError("cm_parameter dataio type with non-cm_parameter public type: %r" % public_type)
977
978 super().__init__(dataio_type, public_type)
979
980 def get_code_cmp(self, location: Location) -> str:
981 return """\
982differ = !cm_are_parameter_equal(&old->{location}, &real_packet->{location});
983""".format(self = self, location = location)
984
985DEFAULT_REGISTRY.dataio_types["cm_parameter"] = CmParameterType
986
987
988class WorklistType(StructType):
989 """Type information for a worklist field"""
990
991 def __init__(self, dataio_type: str, public_type: str):
992 if dataio_type != "worklist":
993 raise ValueError("not a valid worklist type")
994
995 if public_type != "struct worklist":
996 raise ValueError("worklist dataio type with non-worklist public type: %r" % public_type)
997
998 super().__init__(dataio_type, public_type)
999
1000 def get_code_fill(self, location: Location) -> str:
1001 return """\
1002worklist_copy(&real_packet->{location}, {location});
1003""".format(location = location)
1004
1005DEFAULT_REGISTRY.dataio_types["worklist"] = WorklistType
1006
1007
1008class SizedType(BasicType):
1009 """Abstract base class (ABC) for field types that include a size"""
1010
1011 def __init__(self, dataio_type: str, public_type: str, size: SizeInfo):
1012 super().__init__(dataio_type, public_type)
1013 self.size = size
1014 """Size info (maximum and actual) of this type"""
1015
1016 def get_code_declaration(self, location: Location) -> str:
1017 return super().get_code_declaration(
1018 location.deeper("%s[%s]" % (location, self.size.declared))
1019 )
1020
1021 def get_code_handle_param(self, location: Location) -> str:
1022 # add "const" if top level
1023 pre = "" if location.depth else "const "
1024 return pre + super().get_code_handle_param(location.deeper("*%s" % location))
1025
1026 @abstractmethod
1027 def get_code_fill(self, location: Location) -> str:
1028 return super().get_code_fill(location)
1029
1030 def __str__(self) -> str:
1031 return "%s[%s]" % (super().__str__(), self.size)
1032
1033
1034class StringType(SizedType):
1035 """Type information for a string field"""
1036
1037 def __init__(self, dataio_type: str, public_type: str, size: SizeInfo):
1038 if dataio_type not in ("string", "estring"):
1039 raise ValueError("not a valid string type")
1040
1041 if public_type != "char":
1042 raise ValueError("string dataio type with non-char public type: %r" % public_type)
1043
1044 super().__init__(dataio_type, public_type, size)
1045
1046 def get_code_fill(self, location: Location) -> str:
1047 return """\
1048sz_strlcpy(real_packet->{location}, {location});
1049""".format(location = location)
1050
1051 def get_code_cmp(self, location: Location) -> str:
1052 return """\
1053differ = (strcmp(old->{location}, real_packet->{location}) != 0);
1054""".format(self = self, location = location)
1055
1056 def get_code_get(self, location: Location, deep_diff: bool = False) -> str:
1057 return """\
1058if (!DIO_GET({self.dataio_type}, &din, &field_addr, real_packet->{location}, sizeof(real_packet->{location}))) {{
1059 RECEIVE_PACKET_FIELD_ERROR({location.name});
1060}}
1061""".format(self = self, location = location)
1062
1063DEFAULT_REGISTRY.dataio_types["string"] = DEFAULT_REGISTRY.dataio_types["estring"] = partial(NeedSizeType, cls = StringType)
1064
1065
1066class MemoryType(SizedType):
1067 """Type information for a memory field"""
1068
1069 def __init__(self, dataio_type: str, public_type: str, size: SizeInfo):
1070 if dataio_type != "memory":
1071 raise ValueError("not a valid memory type")
1072
1073 super().__init__(dataio_type, public_type, size)
1074
1075 def get_code_fill(self, location: Location) -> str:
1076 raise NotImplementedError("fill not supported for memory-type fields")
1077
1078 def get_code_cmp(self, location: Location) -> str:
1079 if self.size.constant:
1080 return """\
1081differ = (memcmp(old->{location}, real_packet->{location}, {self.size.real}) != 0);
1082""".format(self = self, location = location)
1083 return """\
1084differ = (({self.size.old} != {self.size.real})
1085 || (memcmp(old->{location}, real_packet->{location}, {self.size.real}) != 0));
1086""".format(self = self, location = location)
1087
1088 def get_code_put(self, location: Location, deep_diff: bool = False) -> str:
1089 return """\
1090e |= DIO_PUT({self.dataio_type}, &dout, &field_addr, &real_packet->{location}, {self.size.real});
1091""".format(self = self, location = location)
1092
1093 def get_code_get(self, location: Location, deep_diff: bool = False) -> str:
1094 size_check = self.size.receive_size_check(location.name)
1095 return """\
1096
1097{size_check}\
1098if (!DIO_GET({self.dataio_type}, &din, &field_addr, real_packet->{location}, {self.size.real})) {{
1099 RECEIVE_PACKET_FIELD_ERROR({location.name});
1100}}
1101""".format(self = self, location = location, size_check = size_check)
1102
1103DEFAULT_REGISTRY.dataio_types["memory"] = partial(NeedSizeType, cls = MemoryType)
1104
1105
1106class ArrayType(FieldType):
1107 """Type information for an array field. Consists of size information and
1108 another FieldType for the array's elements, which may also be an
1109 ArrayType (for multi-dimensionaly arrays)."""
1110
1111 def __init__(self, elem: FieldType, size: SizeInfo):
1112 self.elem = elem
1113 """The type of the array elements"""
1114 self.size = size
1115 """The length (maximum and actual) of the array"""
1116
1117 def get_code_declaration(self, location: Location) -> str:
1118 return self.elem.get_code_declaration(
1119 location.deeper("%s[%s]" % (location, self.size.declared))
1120 )
1121
1122 def get_code_handle_param(self, location: Location) -> str:
1123 # add "const" if top level
1124 pre = "" if location.depth else "const "
1125 return pre + self.elem.get_code_handle_param(location.deeper("*%s" % location))
1126
1127 def get_code_fill(self, location: Location) -> str:
1128 inner_fill = prefix(" ", self.elem.get_code_fill(location.sub))
1129 return """\
1130{{
1131 int {location.index};
1132
1133 for ({location.index} = 0; {location.index} < {self.size.real}; {location.index}++) {{
1134{inner_fill}\
1135 }}
1136}}
1137""".format(self = self, location = location, inner_fill = inner_fill)
1138
1139 def get_code_hash(self, location: Location) -> str:
1140 raise ValueError("hash not supported for array type %s in field %s" % (self, location.name))
1141
1142 def get_code_cmp(self, location: Location) -> str:
1143 if not self.size.constant:
1144 head = """\
1145differ = ({self.size.old} != {self.size.real});
1146if (!differ) {{
1147""".format(self = self)
1148 else:
1149 head = """\
1150differ = FALSE;
1151{
1152"""
1153 inner_cmp = prefix(" ", self.elem.get_code_cmp(location.sub))
1154 return head + """\
1155 int {location.index};
1156
1157 for ({location.index} = 0; {location.index} < {self.size.real}; {location.index}++) {{
1158{inner_cmp}\
1159 if (differ) {{
1160 break;
1161 }}
1162 }}
1163}}
1164""".format(self = self, location = location, inner_cmp = inner_cmp)
1165
1166 def _get_code_put_full(self, location: Location, inner_put: str) -> str:
1167 """Helper method. Generate put code without array-diff."""
1168 inner_put = prefix(" ", inner_put)
1169 return """\
1170{{
1171 int {location.index};
1172
1173#ifdef FREECIV_JSON_CONNECTION
1174 /* Create the array. */
1175 e |= DIO_PUT(farray, &dout, &field_addr, {self.size.real});
1176
1177 /* Enter the array. */
1178 field_addr.sub_location = plocation_elem_new(0);
1179#endif /* FREECIV_JSON_CONNECTION */
1180
1181 for ({location.index} = 0; {location.index} < {self.size.real}; {location.index}++) {{
1182#ifdef FREECIV_JSON_CONNECTION
1183 /* Next array element. */
1184 field_addr.sub_location->number = {location.index};
1185#endif /* FREECIV_JSON_CONNECTION */
1186{inner_put}\
1187 }}
1188
1189#ifdef FREECIV_JSON_CONNECTION
1190 /* Exit array. */
1191 FC_FREE(field_addr.sub_location);
1192#endif /* FREECIV_JSON_CONNECTION */
1193}}
1194""".format(self = self, location = location, inner_put = inner_put)
1195
1196 def _get_code_put_diff(self, location: Location, inner_put: str) -> str:
1197 """Helper method. Generate array-diff put code."""
1198 inner_put = prefix(" ", inner_put)
1199 inner_cmp = prefix(" ", self.elem.get_code_cmp(location.sub))
1200 return """\
1201{{
1202 int {location.index};
1203
1204#ifdef FREECIV_JSON_CONNECTION
1205 size_t count_{location.index} = 0;
1206
1207 /* Create the array. */
1208 e |= DIO_PUT(farray, &dout, &field_addr, 0);
1209
1210 /* Enter array. */
1211 field_addr.sub_location = plocation_elem_new(0);
1212#endif /* FREECIV_JSON_CONNECTION */
1213
1214 fc_assert({self.size.real} < MAX_UINT16);
1215
1216 for ({location.index} = 0; {location.index} < {self.size.real}; {location.index}++) {{
1217{inner_cmp}\
1218
1219 if (differ) {{
1220#ifdef FREECIV_JSON_CONNECTION
1221 /* Append next diff array element. */
1222 field_addr.sub_location->number = -1;
1223
1224 /* Create the diff array element. */
1225 e |= DIO_PUT(farray, &dout, &field_addr, 2);
1226
1227 /* Enter diff array element (start at the index address). */
1228 field_addr.sub_location->number = count_{location.index}++;
1229 field_addr.sub_location->sub_location = plocation_elem_new(0);
1230#endif /* FREECIV_JSON_CONNECTION */
1231 e |= DIO_PUT(uint16, &dout, &field_addr, {location.index});
1232
1233#ifdef FREECIV_JSON_CONNECTION
1234 /* Content address. */
1235 field_addr.sub_location->sub_location->number = 1;
1236#endif /* FREECIV_JSON_CONNECTION */
1237{inner_put}\
1238
1239#ifdef FREECIV_JSON_CONNECTION
1240 /* Exit diff array element. */
1241 FC_FREE(field_addr.sub_location->sub_location);
1242#endif /* FREECIV_JSON_CONNECTION */
1243 }}
1244 }}
1245#ifdef FREECIV_JSON_CONNECTION
1246 /* Append diff array element. */
1247 field_addr.sub_location->number = -1;
1248
1249 /* Create the terminating diff array element. */
1250 e |= DIO_PUT(farray, &dout, &field_addr, 1);
1251
1252 /* Enter diff array element (start at the index address). */
1253 field_addr.sub_location->number = count_{location.index};
1254 field_addr.sub_location->sub_location = plocation_elem_new(0);
1255#endif /* FREECIV_JSON_CONNECTION */
1256 e |= DIO_PUT(uint16, &dout, &field_addr, MAX_UINT16);
1257
1258#ifdef FREECIV_JSON_CONNECTION
1259 /* Exit diff array element. */
1260 FC_FREE(field_addr.sub_location->sub_location);
1261
1262 /* Exit array. */
1263 FC_FREE(field_addr.sub_location);
1264#endif /* FREECIV_JSON_CONNECTION */
1265}}
1266""".format(self = self, location = location, inner_cmp = inner_cmp, inner_put = inner_put)
1267
1268 def get_code_put(self, location: Location, deep_diff: bool = False) -> str:
1269 inner_put = self.elem.get_code_put(location.sub, deep_diff)
1270 if deep_diff:
1271 return self._get_code_put_diff(location, inner_put)
1272 else:
1273 return self._get_code_put_full(location, inner_put)
1274
1275 def _get_code_get_full(self, location: Location, inner_get: str) -> str:
1276 """Helper method. Generate get code without array-diff."""
1277 size_check = prefix(" ", self.size.receive_size_check(location.name))
1278 inner_get = prefix(" ", inner_get)
1279 return """\
1280{{
1281 int {location.index};
1282
1283#ifdef FREECIV_JSON_CONNECTION
1284 /* Enter array. */
1285 field_addr.sub_location = plocation_elem_new(0);
1286#endif /* FREECIV_JSON_CONNECTION */
1287
1288{size_check}\
1289 for ({location.index} = 0; {location.index} < {self.size.real}; {location.index}++) {{
1290#ifdef FREECIV_JSON_CONNECTION
1291 field_addr.sub_location->number = {location.index};
1292#endif /* FREECIV_JSON_CONNECTION */
1293{inner_get}\
1294 }}
1295
1296#ifdef FREECIV_JSON_CONNECTION
1297 /* Exit array. */
1298 FC_FREE(field_addr.sub_location);
1299#endif /* FREECIV_JSON_CONNECTION */
1300}}
1301""".format(self = self, location = location, inner_get = inner_get, size_check = size_check)
1302
1303 def _get_code_get_diff(self, location: Location, inner_get: str) -> str:
1304 """Helper method. Generate array-diff get code."""
1305 inner_get = prefix(" ", inner_get)
1306 return """\
1307{{
1308#ifdef FREECIV_JSON_CONNECTION
1309 int count;
1310
1311 /* Enter array. */
1312 field_addr.sub_location = plocation_elem_new(0);
1313
1314 for (count = 0;; count++) {{
1315 int {location.index};
1316
1317 field_addr.sub_location->number = count;
1318
1319 /* Enter diff array element (start at the index address). */
1320 field_addr.sub_location->sub_location = plocation_elem_new(0);
1321#else /* FREECIV_JSON_CONNECTION */
1322 while (TRUE) {{
1323 int {location.index};
1324#endif /* FREECIV_JSON_CONNECTION */
1325
1326 if (!DIO_GET(uint16, &din, &field_addr, &{location.index})) {{
1327 RECEIVE_PACKET_FIELD_ERROR({location.name});
1328 }}
1329 if ({location.index} == MAX_UINT16) {{
1330#ifdef FREECIV_JSON_CONNECTION
1331 /* Exit diff array element. */
1332 FC_FREE(field_addr.sub_location->sub_location);
1333
1334 /* Exit diff array. */
1335 FC_FREE(field_addr.sub_location);
1336#endif /* FREECIV_JSON_CONNECTION */
1337
1338 break;
1339 }}
1340 if ({location.index} > {self.size.real}) {{
1341 RECEIVE_PACKET_FIELD_ERROR({location.name},
1342 ": unexpected value %d "
1343 "(> {self.size.real}) in array diff",
1344 {location.index});
1345 }} else {{
1346#ifdef FREECIV_JSON_CONNECTION
1347 /* Content address. */
1348 field_addr.sub_location->sub_location->number = 1;
1349#endif /* FREECIV_JSON_CONNECTION */
1350{inner_get}\
1351 }}
1352
1353#ifdef FREECIV_JSON_CONNECTION
1354 /* Exit diff array element. */
1355 FC_FREE(field_addr.sub_location->sub_location);
1356#endif /* FREECIV_JSON_CONNECTION */
1357 }}
1358
1359#ifdef FREECIV_JSON_CONNECTION
1360 /* Exit array. */
1361 FC_FREE(field_addr.sub_location);
1362#endif /* FREECIV_JSON_CONNECTION */
1363}}
1364""".format(self = self, location = location, inner_get = inner_get)
1365
1366 def get_code_get(self, location: Location, deep_diff: bool = False) -> str:
1367 inner_get = self.elem.get_code_get(location.sub, deep_diff)
1368 if deep_diff:
1369 return self._get_code_get_diff(location, inner_get)
1370 else:
1371 return self._get_code_get_full(location, inner_get)
1372
1373 def __str__(self) -> str:
1374 return "{self.elem}[{self.size}]".format(self = self)
1375
1376
1377class Field:
1378 """A single field of a packet. Consists of a name, type information
1379 (including array sizes) and flags."""
1380
1381 FIELDS_LINE_PATTERN = re.compile(r"^\s*(\w+(?:\‍([^()]*\‍))?)\s+([^;()]*?)\s*;\s*(.*?)\s*$")
1382 """Matches an entire field definition line.
1383
1384 Groups:
1385 - type
1386 - field names and array sizes
1387 - flags"""
1388
1389 FIELD_ARRAY_PATTERN = re.compile(r"^(.+)\[([^][]+)\]$")
1390 """Matches a field definition with one or more array sizes
1391
1392 Groups:
1393 - everything except the final array size
1394 - the final array size"""
1395
1396 @classmethod
1397 def parse(cls, cfg: ScriptConfig, line: str, resolve_type: typing.Callable[[str], RawFieldType]) -> "typing.Iterable[Field]":
1398 """Parse a single line defining one or more fields"""
1399 mo = cls.FIELDS_LINE_PATTERN.fullmatch(line)
1400 if mo is None:
1401 raise ValueError("invalid field definition: %r" % line)
1402 type_text, fields, flags = (i.strip() for i in mo.groups(""))
1403
1404 type_info = resolve_type(type_text)
1405 flag_info = FieldFlags.parse(flags)
1406
1407 # analyze fields
1408 for field_text in fields.split(","):
1409 field_text = field_text.strip()
1410 field_type = type_info
1411
1412 mo = cls.FIELD_ARRAY_PATTERN.fullmatch(field_text)
1413 while mo is not None:
1414 field_text = mo.group(1)
1415 field_type = field_type.array(SizeInfo.parse(mo.group(2)))
1416 mo = cls.FIELD_ARRAY_PATTERN.fullmatch(field_text)
1417
1418 if not isinstance(field_type, FieldType):
1419 raise ValueError("need an array size to use type %s" % field_type)
1420
1421 yield Field(cfg, field_text, field_type, flag_info)
1422
1423 def __init__(self, cfg: ScriptConfig, name: str, type_info: FieldType, flags: FieldFlags):
1424 self.cfg = cfg
1425 """Configuration used when generating code for this field"""
1426 self.name = name
1427 """This field's name (identifier)"""
1428
1429 self.type_info = type_info
1430 """This field's type information; see FieldType"""
1431 self.flags = flags
1432 """This field's flags; see FieldFlags"""
1433
1434 @property
1435 def is_key(self) -> bool:
1436 """Whether this is a key field"""
1437 return self.flags.is_key
1438
1439 @property
1440 def diff(self) -> bool:
1441 """Whether this field uses deep diff / array-diff when transmitted
1442 as part of a delta packet"""
1443 return self.flags.diff
1444
1445 @property
1446 def all_caps(self) -> "typing.AbstractSet[str]":
1447 """Set of all capabilities affecting this field"""
1448 return self.flags.add_caps | self.flags.remove_caps
1449
1450 def present_with_caps(self, caps: typing.Container[str]) -> bool:
1451 """Determine whether this field should be part of a variant with the
1452 given capabilities"""
1453 return (
1454 all(cap in caps for cap in self.flags.add_caps)
1455 ) and (
1456 all(cap not in caps for cap in self.flags.remove_caps)
1457 )
1458
1459 def get_declar(self) -> str:
1460 """Generate the way this field is declared in the packet struct"""
1461 return self.type_info.get_code_declaration(Location(self.name))
1462
1463 def get_handle_param(self) -> str:
1464 """Generate the way this field is declared as a parameter of a
1465 handle function.
1466
1467 See also self.get_handle_arg()"""
1468 return self.type_info.get_code_handle_param(Location(self.name))
1469
1470 def get_handle_arg(self, packet_arrow: str) -> str:
1471 """Generate the way this field is passed as an argument to a handle
1472 function.
1473
1474 See also self.get_handle_param()"""
1475 return self.type_info.get_code_handle_arg(Location(
1476 self.name,
1477 packet_arrow + self.name,
1478 ))
1479
1480 def get_fill(self) -> str:
1481 """Generate code moving this field from the dsend arguments into
1482 the packet struct."""
1483 return self.type_info.get_code_fill(Location(self.name))
1484
1485 def get_hash(self) -> str:
1486 """Generate code factoring this field into a hash computation."""
1487 assert self.is_key
1488 return self.type_info.get_code_hash(Location(self.name))
1489
1490 def get_cmp(self) -> str:
1491 """Generate code checking whether this field changed.
1492
1493 This code is primarily used by self.get_cmp_wrapper()"""
1494 return self.type_info.get_code_cmp(Location(self.name))
1495
1496 @property
1497 def folded_into_head(self) -> bool:
1498 """Whether this field is folded into the packet header.
1499
1500 If enabled, lone booleans (which only carry one bit of information)
1501 get directly written into the `fields` bitvector, since they don't
1502 take any more space than the usual "content-differs" bit would.
1503
1504 See also get_cmp_wrapper()"""
1505 return (
1506 self.cfg.fold_bool
1507 and self.type_info.foldable
1508 )
1509
1510 def get_cmp_wrapper(self, i: int, pack: "Variant") -> str:
1511 """Generate code setting this field's bit in the `fields` bitvector.
1512
1513 This bit marks whether the field changed and is being transmitted,
1514 except for (non-array) boolean fields folded into the header;
1515 see self.folded_into_head for more details.
1516
1517 See also self.get_cmp()"""
1518 if self.folded_into_head:
1519 if pack.is_info != "no":
1520 cmp = self.get_cmp()
1521 differ_part = """\
1522if (differ) {
1523 different++;
1524}
1525"""
1526 else:
1527 cmp = ""
1528 differ_part = ""
1529 b = "packet->{self.name}".format(self = self)
1530 return cmp + differ_part + """\
1531if (%s) {
1532 BV_SET(fields, %d);
1533}
1534
1535""" % (b, i)
1536 else:
1537 cmp = self.get_cmp()
1538 if pack.is_info != "no":
1539 return """\
1540%s\
1541if (differ) {
1542 different++;
1543 BV_SET(fields, %d);
1544}
1545
1546""" % (cmp, i)
1547 else:
1548 return """\
1549%s\
1550if (differ) {
1551 BV_SET(fields, %d);
1552}
1553
1554""" % (cmp, i)
1555
1556 def get_put_wrapper(self, packet: "Variant", i: int, deltafragment: bool) -> str:
1557 """Generate code conditionally putting this field iff its bit in the
1558 `fields` bitvector is set.
1559
1560 Does nothing for boolean fields folded into the packet header.
1561
1562 See also self.get_put()"""
1563 if self.folded_into_head:
1564 return """\
1565/* field {i:d} is folded into the header */
1566""".format(i = i)
1567 put = prefix(" ", self.get_put(deltafragment))
1568 if packet.gen_log:
1569 f = """\
1570 {packet.log_macro}(" field \'{self.name}\' has changed");
1571""".format(packet = packet, self = self)
1572 else:
1573 f=""
1574 if packet.gen_stats:
1575 s = """\
1576 stats_{packet.name}_counters[{i:d}]++;
1577""".format(packet = packet, i = i)
1578 else:
1579 s=""
1580 return """\
1581if (BV_ISSET(fields, {i:d})) {{
1582{f}\
1583{s}\
1584{put}\
1585}}
1586""".format(i = i, f = f, s = s, put = put)
1587
1588 def get_put(self, deltafragment: bool) -> str:
1589 """Generate the code putting this field, i.e. writing it to the
1590 dataio stream.
1591
1592 This does not include delta-related code checking whether to
1593 transmit the field in the first place; see self.get_put_wrapper()"""
1594 real = self.get_put_real(deltafragment)
1595 return """\
1596#ifdef FREECIV_JSON_CONNECTION
1597field_addr.name = "{self.name}";
1598#endif /* FREECIV_JSON_CONNECTION */
1599e = 0;
1600{real}\
1601if (e) {{
1602 log_packet_detailed("'{self.name}' field error detected");
1603}}
1604""".format(self = self, real = real)
1605
1606 def get_put_real(self, deltafragment: bool) -> str:
1607 """Generate the bare core of this field's put code. This code is not
1608 yet wrapped for full delta and JSON protocol support.
1609
1610 See self.get_put() for more info"""
1611 return self.type_info.get_code_put(Location(self.name), deltafragment and self.diff)
1612
1613 def get_get_wrapper(self, packet: "Variant", i: int, deltafragment: bool) -> str:
1614 """Generate code conditionally getting this field iff its bit in the
1615 `fields` bitvector is set.
1616
1617 For boolean fields folded into the packet header, instead reads the
1618 field from the bitvector.
1619
1620 See also self.get_get()"""
1621 if self.folded_into_head:
1622 return """\
1623real_packet->{self.name} = BV_ISSET(fields, {i:d});
1624""".format(self = self, i = i)
1625 get = prefix(" ", self.get_get(deltafragment))
1626 if packet.gen_log:
1627 f = """\
1628 {packet.log_macro}(" got field '{self.name}'");
1629""".format(self = self, packet = packet)
1630 else:
1631 f=""
1632 return """\
1633if (BV_ISSET(fields, {i:d})) {{
1634{f}\
1635{get}\
1636}}
1637""".format(i = i, f = f, get = get)
1638
1639 def get_get(self, deltafragment: bool) -> str:
1640 """Generate the code getting this field, i.e. reading it from the
1641 dataio stream.
1642
1643 This does not include delta-related code checking if the field
1644 was transmitted in the first place; see self.get_get_wrapper()"""
1645 return """\
1646#ifdef FREECIV_JSON_CONNECTION
1647field_addr.name = \"{self.name}\";
1648#endif /* FREECIV_JSON_CONNECTION */
1649""".format(self = self) + self.get_get_real(deltafragment)
1650
1651 def get_get_real(self, deltafragment: bool) -> str:
1652 """Generate the bare core of this field's get code. This code is not
1653 yet wrapped for full delta and JSON protocol support.
1654
1655 See self.get_get() for more info"""
1656 return self.type_info.get_code_get(Location(self.name), deltafragment and self.diff)
1657
1658
1659class Variant:
1660 """Represents one variant of a packet. Packets with add-cap or
1661 remove-cap fields have different variants for different combinations of
1662 the relevant optional capabilities."""
1663
1664 def __init__(self, poscaps: typing.Iterable[str], negcaps: typing.Iterable[str],
1665 packet: "Packet", no: int):
1666 self.packet = packet
1667 """The packet this is a variant of"""
1668 self.no=no
1669 """The numeric variant ID (not packet ID) of this variant"""
1670 self.name = "%s_%d" % (packet.name, no)
1671 """The full name of this variant"""
1672
1673 self.poscaps = set(poscaps)
1674 """The set of optional capabilities that must be present to use this
1675 variant"""
1676 self.negcaps = set(negcaps)
1677 """The set of optional capabilities that must *not* be present to
1678 use this variant"""
1679 self.fields = [
1680 field
1681 for field in packet.fields
1682 if field.present_with_caps(self.poscaps)
1683 ]
1684 """All fields that are transmitted when using this variant"""
1685 self.key_fields = [field for field in self.fields if field.is_key]
1686 """The key fields that are used for this variant"""
1687 self.other_fields = [field for field in self.fields if not field.is_key]
1688 """The non-key fields that are transmitted when using this variant"""
1689 # FIXME: Doesn't work with non-int key fields
1690 self.keys_format=", ".join(["%d"]*len(self.key_fields))
1691 """The printf format string for this variant's key fields in
1692 generated log calls
1693
1694 See also self.keys_arg"""
1695 self.keys_arg = ", ".join("real_packet->" + field.name for field in self.key_fields)
1696 """The arguments passed when formatting this variant's key fields in
1697 generated log calls
1698
1699 See also self.keys_format"""
1700 if self.keys_arg:
1701 self.keys_arg=",\n "+self.keys_arg
1702
1703 if not self.fields and packet.fields:
1704 raise ValueError("empty variant for nonempty {self.packet_name} with capabilities {self.poscaps}".format(self = self))
1705
1706 @property
1707 def cfg(self) -> ScriptConfig:
1708 """Configuration used when generating code for this packet
1709 variant
1710
1711 See self.packet and Packet.cfg"""
1712 return self.packet.cfg
1713
1714 @property
1715 def gen_stats(self) -> bool:
1716 """Whether to generate delta stats code for this packet variant
1717
1718 See self.cfg and ScriptConfig.gen_stats"""
1719 return self.cfg.gen_stats
1720
1721 @property
1722 def log_macro(self) -> "str | None":
1723 """The log macro used to generate log calls for this packet variant,
1724 or None if no log calls should be generated
1725
1726 See self.cfg and ScriptConfig.log_macro"""
1727 return self.cfg.log_macro
1728
1729 @property
1730 def gen_log(self) -> bool:
1731 """Whether to generate log calls for this packet variant
1732
1733 See self.log_macro"""
1734 return self.log_macro is not None
1735
1736 @property
1737 def packet_name(self) -> str:
1738 """Name of the packet this is a variant of
1739
1740 See Packet.name"""
1741 return self.packet.name
1742
1743 @property
1744 def type(self) -> str:
1745 """Type (enum constant) of the packet this is a variant of
1746
1747 See Packet.type"""
1748 return self.packet.type
1749
1750 @property
1751 def no_packet(self) -> bool:
1752 """Whether the send function should not take/need a packet struct
1753
1754 See Packet.no_packet"""
1755 return self.packet.no_packet
1756
1757 @property
1758 def delta(self) -> bool:
1759 """Whether this packet can use delta optimization
1760
1761 See Packet.delta"""
1762 return self.packet.delta
1763
1764 @property
1765 def want_force(self):
1766 """Whether send function takes a force_to_send boolean
1767
1768 See Packet.want_force"""
1769 return self.packet.want_force
1770
1771 @property
1772 def is_info(self) -> str:
1773 """Whether this is an info or game-info packet"""
1774 return self.packet.is_info
1775
1776 @property
1777 def cancel(self) -> "list[str]":
1778 """List of packets to cancel when sending or receiving this packet
1779
1780 See Packet.cancel"""
1781 return self.packet.cancel
1782
1783 @property
1784 def differ_used(self) -> bool:
1785 """Whether the send function needs a `differ` boolean.
1786
1787 See get_send()"""
1788 return (
1789 (not self.no_packet)
1790 and self.delta
1791 and (
1792 self.is_info != "no"
1793 or any(
1794 not field.folded_into_head
1795 for field in self.other_fields
1796 )
1797 )
1798 )
1799
1800 @property
1801 def condition(self) -> str:
1802 """The condition determining whether this variant should be used,
1803 based on capabilities.
1804
1805 See get_packet_handlers_fill_capability()"""
1806 if self.poscaps or self.negcaps:
1807 cap_fmt = "has_capability(\"%s\", capability)"
1808 return " && ".join(chain(
1809 (cap_fmt % cap for cap in sorted(self.poscaps)),
1810 ("!" + cap_fmt % cap for cap in sorted(self.negcaps)),
1811 ))
1812 else:
1813 return "TRUE"
1814
1815 @property
1816 def bits(self) -> int:
1817 """The length of the bitvector for this variant."""
1818 return len(self.other_fields)
1819
1820 @property
1821 def receive_prototype(self) -> str:
1822 """The prototype of this variant's receive function"""
1823 return "static struct {self.packet_name} *receive_{self.name}(struct connection *pc)".format(self = self)
1824
1825 @property
1826 def send_prototype(self) -> str:
1827 """The prototype of this variant's send function"""
1828 return "static int send_{self.name}(struct connection *pc{self.packet.extra_send_args})".format(self = self)
1829
1830 @property
1831 def send_handler(self) -> str:
1832 """Code to set the send handler for this variant
1833
1834 See get_packet_handlers_fill_initial and
1835 get_packet_handlers_fill_capability"""
1836 if self.no_packet:
1837 return """\
1838phandlers->send[{self.type}].no_packet = (int(*)(struct connection *)) send_{self.name};
1839""".format(self = self)
1840 elif self.want_force:
1841 return """\
1842phandlers->send[{self.type}].force_to_send = (int(*)(struct connection *, const void *, bool)) send_{self.name};
1843""".format(self = self)
1844 else:
1845 return """\
1846phandlers->send[{self.type}].packet = (int(*)(struct connection *, const void *)) send_{self.name};
1847""".format(self = self)
1848
1849 @property
1850 def receive_handler(self) -> str:
1851 """Code to set the receive handler for this variant
1852
1853 See get_packet_handlers_fill_initial and
1854 get_packet_handlers_fill_capability"""
1855 return """\
1856phandlers->receive[{self.type}] = (void *(*)(struct connection *)) receive_{self.name};
1857""".format(self = self)
1858
1859 def get_stats(self) -> str:
1860 """Generate the declaration of the delta stats counters associated
1861 with this packet variant"""
1862 names = ", ".join(
1863 "\"%s\"" % field.name
1864 for field in self.other_fields
1865 )
1866
1867 return """\
1868static int stats_{self.name}_sent;
1869static int stats_{self.name}_discarded;
1870static int stats_{self.name}_counters[{self.bits:d}];
1871static char *stats_{self.name}_names[] = {{{names}}};
1872
1873""".format(self = self, names = names)
1874
1875 def get_bitvector(self) -> str:
1876 """Generate the declaration of the fields bitvector type for this
1877 packet variant"""
1878 return """\
1879BV_DEFINE({self.name}_fields, {self.bits});
1880""".format(self = self)
1881
1882 def get_report_part(self) -> str:
1883 """Generate the part of the delta_stats_report8) function specific
1884 to this packet variant"""
1885 return """\
1886
1887if (stats_{self.name}_sent > 0
1888 && stats_{self.name}_discarded != stats_{self.name}_sent) {{
1889 log_test(\"{self.name} %d out of %d got discarded\",
1890 stats_{self.name}_discarded, stats_{self.name}_sent);
1891 for (i = 0; i < {self.bits}; i++) {{
1892 if (stats_{self.name}_counters[i] > 0) {{
1893 log_test(\" %4d / %4d: %2d = %s\",
1894 stats_{self.name}_counters[i],
1895 (stats_{self.name}_sent - stats_{self.name}_discarded),
1896 i, stats_{self.name}_names[i]);
1897 }}
1898 }}
1899}}
1900""".format(self = self)
1901
1902 def get_reset_part(self) -> str:
1903 """Generate the part of the delta_stats_reset() function specific
1904 to this packet variant"""
1905 return """\
1906stats_{self.name}_sent = 0;
1907stats_{self.name}_discarded = 0;
1908memset(stats_{self.name}_counters, 0,
1909 sizeof(stats_{self.name}_counters));
1910""".format(self = self)
1911
1912 def get_hash(self) -> str:
1913 """Generate the key hash function for this variant"""
1914 if not self.key_fields:
1915 return """\
1916#define hash_{self.name} hash_const
1917
1918""".format(self = self)
1919
1920 intro = """\
1921static genhash_val_t hash_{self.name}(const void *vkey)
1922{{
1923 const struct {self.packet_name} *key = (const struct {self.packet_name} *) vkey;
1924 genhash_val_t result = 0;
1925
1926""".format(self = self)
1927
1928 body = """\
1929
1930 result *= 5;
1931
1932""".join(prefix(" ", field.get_hash()) for field in self.key_fields)
1933
1934 extro = """\
1935
1936 result &= 0xFFFFFFFF;
1937 return result;
1938}
1939
1940"""
1941
1942 return intro + body + extro
1943
1944 def get_cmp(self) -> str:
1945 """Generate the key comparison function for this variant"""
1946 if not self.key_fields:
1947 return """\
1948#define cmp_{self.name} cmp_const
1949
1950""".format(self = self)
1951
1952 # note: the names `old` and `real_packet` allow reusing
1953 # field-specific cmp code
1954 intro = """\
1955static bool cmp_{self.name}(const void *vkey1, const void *vkey2)
1956{{
1957 const struct {self.packet_name} *old = (const struct {self.packet_name} *) vkey1;
1958 const struct {self.packet_name} *real_packet = (const struct {self.packet_name} *) vkey2;
1959 bool differ;
1960
1961""".format(self = self)
1962
1963 body = """\
1964
1965 if (differ) {
1966 return !differ;
1967 }
1968
1969""".join(prefix(" ", field.get_cmp()) for field in self.key_fields)
1970
1971 extro = """\
1972
1973 return !differ;
1974}
1975"""
1976
1977 return intro + body + extro
1978
1979 def get_send(self) -> str:
1980 """Generate the send function for this packet variant"""
1981 if self.gen_stats:
1982 report = """\
1983
1984 stats_total_sent++;
1985 stats_{self.name}_sent++;
1986""".format(self = self)
1987 else:
1988 report=""
1989 if self.gen_log:
1990 log = """\
1991
1992 {self.log_macro}("{self.name}: sending info about ({self.keys_format})"{self.keys_arg});
1993""".format(self = self)
1994 else:
1995 log=""
1996
1997 if self.no_packet:
1998 main_header = ""
1999 else:
2000 if self.packet.want_pre_send:
2001 main_header = """\
2002 /* copy packet for pre-send */
2003 struct {self.packet_name} packet_buf = *packet;
2004 const struct {self.packet_name} *real_packet = &packet_buf;
2005""".format(self = self)
2006 else:
2007 main_header = """\
2008 const struct {self.packet_name} *real_packet = packet;
2009""".format(self = self)
2010 main_header += """\
2011 int e;
2012"""
2013
2014 if not self.packet.want_pre_send:
2015 pre = ""
2016 elif self.no_packet:
2017 pre = """\
2018
2019 pre_send_{self.packet_name}(pc, NULL);
2020""".format(self = self)
2021 else:
2022 pre = """\
2023
2024 pre_send_{self.packet_name}(pc, &packet_buf);
2025""".format(self = self)
2026
2027 if not self.no_packet:
2028 if self.delta:
2029 if self.want_force:
2030 diff = "force_to_send"
2031 else:
2032 diff = "0"
2033 delta_header = """\
2034#ifdef FREECIV_DELTA_PROTOCOL
2035 {self.name}_fields fields;
2036 struct {self.packet_name} *old;
2037""".format(self = self)
2038 if self.differ_used:
2039 delta_header += """\
2040 bool differ;
2041"""
2042 delta_header += """\
2043 struct genhash **hash = pc->phs.sent + {self.type};
2044""".format(self = self)
2045 if self.is_info != "no":
2046 delta_header += """\
2047 int different = {diff};
2048""".format(diff = diff)
2049 delta_header += """\
2050#endif /* FREECIV_DELTA_PROTOCOL */
2051"""
2052 body = prefix(" ", self.get_delta_send_body()) + """\
2053#ifndef FREECIV_DELTA_PROTOCOL
2054"""
2055 else:
2056 delta_header=""
2057 body = """\
2058#if 1 /* To match endif */
2059"""
2060 body += "".join(
2061 prefix(" ", field.get_put(False))
2062 for field in self.fields
2063 )
2064 body += """\
2065
2066#endif
2067"""
2068 else:
2069 body=""
2070 delta_header=""
2071
2072 if self.packet.want_post_send:
2073 if self.no_packet:
2074 post = """\
2075 post_send_{self.packet_name}(pc, NULL);
2076""".format(self = self)
2077 else:
2078 post = """\
2079 post_send_{self.packet_name}(pc, real_packet);
2080""".format(self = self)
2081 else:
2082 post=""
2083
2084 if self.fields:
2085 faddr = """\
2086#ifdef FREECIV_JSON_CONNECTION
2087 struct plocation field_addr;
2088 {
2089 struct plocation *field_addr_tmp = plocation_field_new(NULL);
2090 field_addr = *field_addr_tmp;
2091 FC_FREE(field_addr_tmp);
2092 }
2093#endif /* FREECIV_JSON_CONNECTION */
2094"""
2095 else:
2096 faddr = ""
2097
2098 return "".join((
2099 """\
2100{self.send_prototype}
2101{{
2102""".format(self = self),
2103 main_header,
2104 delta_header,
2105 """\
2106 SEND_PACKET_START({self.type});
2107""".format(self = self),
2108 faddr,
2109 log,
2110 report,
2111 pre,
2112 body,
2113 post,
2114 """\
2115 SEND_PACKET_END({self.type});
2116}}
2117
2118""".format(self = self),
2119 ))
2120
2121 def get_delta_send_body(self, before_return: str = "") -> str:
2122 """Helper for get_send(). Generate the part of the send function
2123 that computes and transmits the delta between the real packet and
2124 the last cached packet."""
2125 intro = """\
2126
2127#ifdef FREECIV_DELTA_PROTOCOL
2128if (NULL == *hash) {{
2129 *hash = genhash_new_full(hash_{self.name}, cmp_{self.name},
2130 NULL, NULL, NULL, free);
2131}}
2132BV_CLR_ALL(fields);
2133
2134if (!genhash_lookup(*hash, real_packet, (void **) &old)) {{
2135 old = fc_malloc(sizeof(*old));
2136 *old = *real_packet;
2137 genhash_insert(*hash, old, old);
2138 memset(old, 0, sizeof(*old));
2139""".format(self = self)
2140 if self.is_info != "no":
2141 intro += """\
2142 different = 1; /* Force to send. */
2143"""
2144 intro += """\
2145}
2146"""
2147 body = "".join(
2148 field.get_cmp_wrapper(i, self)
2149 for i, field in enumerate(self.other_fields)
2150 )
2151 if self.gen_log:
2152 fl = """\
2153 {self.log_macro}(" no change -> discard");
2154""".format(self = self)
2155 else:
2156 fl=""
2157 if self.gen_stats:
2158 s = """\
2159 stats_{self.name}_discarded++;
2160""".format(self = self)
2161 else:
2162 s=""
2163
2164 if self.is_info != "no":
2165 body += """\
2166if (different == 0) {{
2167{fl}\
2168{s}\
2169{before_return}\
2170 return 0;
2171}}
2172""".format(fl = fl, s = s, before_return = before_return)
2173
2174 body += """\
2175
2176#ifdef FREECIV_JSON_CONNECTION
2177field_addr.name = "fields";
2178#endif /* FREECIV_JSON_CONNECTION */
2179e = 0;
2180e |= DIO_BV_PUT(&dout, &field_addr, fields);
2181if (e) {
2182 log_packet_detailed("fields bitvector error detected");
2183}
2184"""
2185
2186 body += "".join(
2187 field.get_put(True)
2188 for field in self.key_fields
2189 )
2190 body += "\n"
2191
2192 body += "".join(
2193 field.get_put_wrapper(self, i, True)
2194 for i, field in enumerate(self.other_fields)
2195 )
2196 body += """\
2197
2198*old = *real_packet;
2199"""
2200
2201 # Cancel some is-info packets.
2202 for i in self.cancel:
2203 body += """\
2204
2205hash = pc->phs.sent + %s;
2206if (NULL != *hash) {
2207 genhash_remove(*hash, real_packet);
2208}
2209""" % i
2210 body += """\
2211#endif /* FREECIV_DELTA_PROTOCOL */
2212"""
2213
2214 return intro+body
2215
2216 def get_receive(self) -> str:
2217 """Generate the receive function for this packet variant"""
2218 if self.delta:
2219 delta_header = """\
2220#ifdef FREECIV_DELTA_PROTOCOL
2221 {self.name}_fields fields;
2222 struct {self.packet_name} *old;
2223 struct genhash **hash = pc->phs.received + {self.type};
2224#endif /* FREECIV_DELTA_PROTOCOL */
2225""".format(self = self)
2226 delta_body1 = """\
2227
2228#ifdef FREECIV_DELTA_PROTOCOL
2229#ifdef FREECIV_JSON_CONNECTION
2230 field_addr.name = "fields";
2231#endif /* FREECIV_JSON_CONNECTION */
2232 DIO_BV_GET(&din, &field_addr, fields);
2233"""
2234 body1 = "".join(
2235 prefix(" ", field.get_get(True))
2236 for field in self.key_fields
2237 )
2238 body1 += """\
2239
2240#else /* FREECIV_DELTA_PROTOCOL */
2241"""
2242 body2 = prefix(" ", self.get_delta_receive_body())
2243 else:
2244 delta_header=""
2245 delta_body1=""
2246 body1 = """\
2247#if 1 /* To match endif */
2248"""
2249 body2=""
2250 nondelta = "".join(
2251 prefix(" ", field.get_get(False))
2252 for field in self.fields
2253 ) or """\
2254 real_packet->__dummy = 0xff;
2255"""
2256 body1 += nondelta + """\
2257#endif
2258"""
2259
2260 if self.gen_log:
2261 log = """\
2262 {self.log_macro}("{self.name}: got info about ({self.keys_format})"{self.keys_arg});
2263""".format(self = self)
2264 else:
2265 log=""
2266
2267 if self.packet.want_post_recv:
2268 post = """\
2269 post_receive_{self.packet_name}(pc, real_packet);
2270""".format(self = self)
2271 else:
2272 post=""
2273
2274 if self.fields:
2275 faddr = """\
2276#ifdef FREECIV_JSON_CONNECTION
2277 struct plocation field_addr;
2278 {
2279 struct plocation *field_addr_tmp = plocation_field_new(NULL);
2280 field_addr = *field_addr_tmp;
2281 FC_FREE(field_addr_tmp);
2282 }
2283#endif /* FREECIV_JSON_CONNECTION */
2284"""
2285 else:
2286 faddr = ""
2287
2288 return "".join((
2289 """\
2290{self.receive_prototype}
2291{{
2292""".format(self = self),
2293 delta_header,
2294 """\
2295 RECEIVE_PACKET_START({self.packet_name}, real_packet);
2296""".format(self = self),
2297 faddr,
2298 delta_body1,
2299 body1,
2300 log,
2301 body2,
2302 post,
2303 """\
2304 RECEIVE_PACKET_END(real_packet);
2305}
2306
2307""",
2308 ))
2309
2310 def get_delta_receive_body(self) -> str:
2311 """Helper for get_receive(). Generate the part of the receive
2312 function responsible for recreating the full packet from the
2313 received delta and the last cached packet."""
2314 if self.key_fields:
2315 # bit-copy the values, since we're moving (not cloning)
2316 # the key fields
2317 # FIXME: might not work for arrays
2318 backup_key = "".join(
2319 prefix(" ", field.get_declar())
2320 for field in self.key_fields
2321 ) + "\n"+ "".join(
2322 """\
2323 {field.name} = real_packet->{field.name};
2324""".format(field = field)
2325 for field in self.key_fields
2326 ) + "\n"
2327 restore_key = "\n" + "".join(
2328 """\
2329 real_packet->{field.name} = {field.name};
2330""".format(field = field)
2331 for field in self.key_fields
2332 )
2333 else:
2334 backup_key = restore_key = ""
2335 if self.gen_log:
2336 fl = """\
2337 {self.log_macro}(" no old info");
2338""".format(self = self)
2339 else:
2340 fl=""
2341 body = """\
2342
2343#ifdef FREECIV_DELTA_PROTOCOL
2344if (NULL == *hash) {{
2345 *hash = genhash_new_full(hash_{self.name}, cmp_{self.name},
2346 NULL, NULL, NULL, free);
2347}}
2348
2349if (genhash_lookup(*hash, real_packet, (void **) &old)) {{
2350 *real_packet = *old;
2351}} else {{
2352{backup_key}\
2353{fl}\
2354 memset(real_packet, 0, sizeof(*real_packet));
2355{restore_key}\
2356}}
2357
2358""".format(self = self, backup_key = backup_key, restore_key = restore_key, fl = fl)
2359 body += "".join(
2360 field.get_get_wrapper(self, i, True)
2361 for i, field in enumerate(self.other_fields)
2362 )
2363
2364 extro = """\
2365
2366if (NULL == old) {
2367 old = fc_malloc(sizeof(*old));
2368 *old = *real_packet;
2369 genhash_insert(*hash, old, old);
2370} else {
2371 *old = *real_packet;
2372}
2373"""
2374
2375 # Cancel some is-info packets.
2376 extro += "".join(
2377 """\
2378
2379hash = pc->phs.received + %s;
2380if (NULL != *hash) {
2381 genhash_remove(*hash, real_packet);
2382}
2383""" % cancel_pack
2384 for cancel_pack in self.cancel
2385 )
2386
2387 return body + extro + """\
2388
2389#endif /* FREECIV_DELTA_PROTOCOL */
2390"""
2391
2392
2393class Directions(Enum):
2394 """Describes the possible combinations of directions for which a packet
2395 can be valid"""
2396
2397 # Note: "sc" and "cs" are used to match the packet flags
2398
2399 DOWN_ONLY = frozenset({"sc"})
2400 """Packet may only be sent from server to client"""
2401
2402 UP_ONLY = frozenset({"cs"})
2403 """Packet may only be sent from client to server"""
2404
2405 UNRESTRICTED = frozenset({"sc", "cs"})
2406 """Packet may be sent both ways"""
2407
2408 @property
2409 def down(self) -> bool:
2410 """Whether a packet may be sent from server to client"""
2411 return "sc" in self.value
2412
2413 @property
2414 def up(self) -> bool:
2415 """Whether a packet may be sent from client to server"""
2416 return "cs" in self.value
2417
2418
2419 class Packet:
2420 """Represents a single packet type (possibly with multiple variants)"""
2421
2422 CANCEL_PATTERN = re.compile(r"^cancel\‍((.*)\‍)$")
2423 """Matches a cancel flag
2424
2425 Groups:
2426 - the packet type to cancel"""
2427
2428 is_info = "no"
2429 """Whether this is an is-info or is-game-info packet.
2430 "no" means normal, "yes" means is-info, "game" means is-game-info"""
2431
2432 want_dsend = False
2433 """Whether to generate a direct-send function taking field values
2434 instead of a packet struct"""
2435
2436 want_lsend = False
2437 """Whether to generate a list-send function sending a packet to
2438 multiple connections"""
2439
2440 want_force = False
2441 """Whether send functions should take a force_to_send parameter
2442 to override discarding is-info packets where nothing changed"""
2443
2444 want_pre_send = False
2445 """Whether a pre-send hook should be called when sending this packet"""
2446
2447 want_post_send = False
2448 """Whether a post-send hook should be called after sending this packet"""
2449
2450 want_post_recv = False
2451 """Wheter a post-receive hook should be called when receiving this
2452 packet"""
2453
2454 delta = True
2455 """Whether to use delta optimization for this packet"""
2456
2457 no_handle = False
2458 """Whether this packet should *not* be handled normally"""
2459
2460 handle_via_packet = True
2461 """Whether to pass the entire packet (by reference) to the handle
2462 function (rather than each field individually)"""
2463
2464 handle_per_conn = False
2465 """Whether this packet's handle function should be called with the
2466 connection instead of the attached player"""
2467
2468 def __init__(self, cfg: ScriptConfig, packet_type: str, packet_number: int, flags_text: str,
2469 lines: typing.Iterable[str], resolve_type: typing.Callable[[str], RawFieldType]):
2470 self.cfg = cfg
2471 """Configuration used when generating code for this packet"""
2472 self.type = packet_type
2473 """The packet type in allcaps (PACKET_FOO), as defined in the
2474 packet_type enum
2475
2476 See also self.name"""
2477 self.type_number = packet_number
2478 """The numeric ID of this packet type"""
2479
2480 # FIXME: Once we can use Python 3.6 features, use variable
2481 # annotations instead of empty comprehensions to set element type
2482 self.cancel = [str(_) for _ in ()]
2483 """List of packet types to drop from the cache when sending or
2484 receiving this packet type"""
2485 dirs = set()
2486
2487 for flag in flags_text.split(","):
2488 flag = flag.strip()
2489 if not flag:
2490 continue
2491
2492 if flag in ("sc", "cs"):
2493 dirs.add(flag)
2494 continue
2495 if flag == "is-info":
2496 self.is_info = "yes"
2497 continue
2498 if flag == "is-game-info":
2499 self.is_info = "game"
2500 continue
2501 if flag == "dsend":
2502 self.want_dsend = True
2503 continue
2504 if flag == "lsend":
2505 self.want_lsend = True
2506 continue
2507 if flag == "force":
2508 self.want_force = True
2509 continue
2510 if flag == "pre-send":
2511 self.want_pre_send = True
2512 continue
2513 if flag == "post-send":
2514 self.want_post_send = True
2515 continue
2516 if flag == "post-recv":
2517 self.want_post_recv = True
2518 continue
2519 if flag == "no-delta":
2520 self.delta = False
2521 continue
2522 if flag == "no-handle":
2523 self.no_handle = True
2524 continue
2525 if flag == "handle-via-fields":
2526 self.handle_via_packet = False
2527 continue
2528 if flag == "handle-per-conn":
2529 self.handle_per_conn = True
2530 continue
2531
2532 mo = __class__.CANCEL_PATTERN.fullmatch(flag)
2533 if mo is not None:
2534 self.cancel.append(mo.group(1))
2535 continue
2536
2537 raise ValueError("unrecognized flag for %s: %r" % (self.name, flag))
2538
2539 if not dirs:
2540 raise ValueError("no directions defined for %s" % self.name)
2541 self.dirs = Directions(dirs)
2542 """Which directions this packet can be sent in"""
2543
2544 self.fields = [
2545 field
2546 for line in lines
2547 for field in Field.parse(self.cfg, line, resolve_type)
2548 ]
2549 """List of all fields of this packet"""
2550 self.key_fields = [field for field in self.fields if field.is_key]
2551 """List of only the key fields of this packet"""
2552 self.other_fields = [field for field in self.fields if not field.is_key]
2553 """List of only the non-key fields of this packet"""
2554
2555 # valid, since self.fields is already set
2556 if self.no_packet:
2557 self.delta = False
2558 self.handle_via_packet = False
2559
2560 if self.want_dsend:
2561 raise ValueError("requested dsend for %s without fields isn't useful" % self.name)
2562
2563 # create cap variants
2564 all_caps = self.all_caps # valid, since self.fields is already set
2565 self.variants = [
2566 Variant(caps, all_caps.difference(caps), self, i + 100)
2567 for i, caps in enumerate(powerset(sorted(all_caps)))
2568 ]
2569 """List of all variants of this packet"""
2570
2571 @property
2572 def name(self) -> str:
2573 """Snake-case name of this packet type"""
2574 return self.type.lower()
2575
2576 @property
2577 def no_packet(self) -> bool:
2578 """Whether this packet's send functions should take no packet
2579 argument. This is the case iff this packet has no fields."""
2580 return not self.fields
2581
2582 @property
2583 def extra_send_args(self) -> str:
2584 """Argements for the regular send function"""
2585 return (
2586 ", const struct {self.name} *packet".format(self = self) if not self.no_packet else ""
2587 ) + (
2588 ", bool force_to_send" if self.want_force else ""
2589 )
2590
2591 @property
2592 def extra_send_args2(self) -> str:
2593 """Arguments passed from lsend to send
2594
2595 See also extra_send_args"""
2596 assert self.want_lsend
2597 return (
2598 ", packet" if not self.no_packet else ""
2599 ) + (
2600 ", force_to_send" if self.want_force else ""
2601 )
2602
2603 @property
2604 def extra_send_args3(self) -> str:
2605 """Arguments for the dsend and dlsend functions"""
2606 assert self.want_dsend
2607 return "".join(
2608 ", %s" % field.get_handle_param()
2609 for field in self.fields
2610 ) + (", bool force_to_send" if self.want_force else "")
2611
2612 @property
2613 def send_prototype(self) -> str:
2614 """Prototype for the regular send function"""
2615 return "int send_{self.name}(struct connection *pc{self.extra_send_args})".format(self = self)
2616
2617 @property
2618 def lsend_prototype(self) -> str:
2619 """Prototype for the lsend function (takes a list of connections)"""
2620 assert self.want_lsend
2621 return "void lsend_{self.name}(struct conn_list *dest{self.extra_send_args})".format(self = self)
2622
2623 @property
2624 def dsend_prototype(self) -> str:
2625 """Prototype for the dsend function (directly takes values instead of a packet struct)"""
2626 assert self.want_dsend
2627 return "int dsend_{self.name}(struct connection *pc{self.extra_send_args3})".format(self = self)
2628
2629 @property
2630 def dlsend_prototype(self) -> str:
2631 """Prototype for the dlsend function (directly takes values; list of connections)"""
2632 assert self.want_dsend
2633 assert self.want_lsend
2634 return "void dlsend_{self.name}(struct conn_list *dest{self.extra_send_args3})".format(self = self)
2635
2636 @property
2637 def all_caps(self) -> "set[str]":
2638 """Set of all capabilities affecting this packet"""
2639 return {cap for field in self.fields for cap in field.all_caps}
2640
2641
2642 def get_struct(self) -> str:
2643 """Generate the struct definition for this packet"""
2644 intro = """\
2645struct {self.name} {{
2646""".format(self = self)
2647 extro = """\
2648};
2649
2650"""
2651
2652 body = "".join(
2653 prefix(" ", field.get_declar())
2654 for field in chain(self.key_fields, self.other_fields)
2655 ) or """\
2656 char __dummy; /* to avoid malloc(0); */
2657"""
2658 return intro+body+extro
2659
2660 def get_prototypes(self) -> str:
2661 """Generate the header prototype declarations for the public
2662 functions associated with this packet."""
2663 result = """\
2664{self.send_prototype};
2665""".format(self = self)
2666 if self.want_lsend:
2667 result += """\
2668{self.lsend_prototype};
2669""".format(self = self)
2670 if self.want_dsend:
2671 result += """\
2672{self.dsend_prototype};
2673""".format(self = self)
2674 if self.want_lsend:
2675 result += """\
2676{self.dlsend_prototype};
2677""".format(self = self)
2678 return result + "\n"
2679
2680 def get_stats(self) -> str:
2681 """Generate the code declaring counters for this packet's variants.
2682
2683 See Variant.get_stats()"""
2684 return "".join(v.get_stats() for v in self.variants)
2685
2686 def get_report_part(self) -> str:
2687 """Generate this packet's part of the delta_stats_report() function.
2688
2689 See Variant.get_report_part() and
2690 PacketsDefinition.code_delta_stats_report"""
2691 return "".join(v.get_report_part() for v in self.variants)
2692
2693 def get_reset_part(self) -> str:
2694 """Generate this packet's part of the delta_stats_reset() function.
2695
2696 See Variant.get_reset_part() and
2697 PacketsDefinition.code_delta_stats_reset"""
2698 return "\n".join(v.get_reset_part() for v in self.variants)
2699
2700 def get_send(self) -> str:
2701 """Generate the implementation of the send function, which sends a
2702 given packet to a given connection."""
2703 if self.no_packet:
2704 func="no_packet"
2705 args=""
2706 elif self.want_force:
2707 func="force_to_send"
2708 args=", packet, force_to_send"
2709 else:
2710 func="packet"
2711 args=", packet"
2712
2713 return """\
2714{self.send_prototype}
2715{{
2716 if (!pc->used) {{
2717 log_error("WARNING: trying to send data to the closed connection %s",
2718 conn_description(pc));
2719 return -1;
2720 }}
2721 fc_assert_ret_val_msg(pc->phs.handlers->send[{self.type}].{func} != NULL, -1,
2722 "Handler for {self.type} not installed");
2723 return pc->phs.handlers->send[{self.type}].{func}(pc{args});
2724}}
2725
2726""".format(self = self, func = func, args = args)
2727
2728 def get_variants(self) -> str:
2729 """Generate all code associated with individual variants of this
2730 packet; see the Variant class (and its methods) for details."""
2731 result=""
2732 for v in self.variants:
2733 if v.delta:
2734 result += """\
2735#ifdef FREECIV_DELTA_PROTOCOL
2736"""
2737 result += v.get_hash()
2738 result += v.get_cmp()
2739 result += v.get_bitvector()
2740 result += """\
2741#endif /* FREECIV_DELTA_PROTOCOL */
2742
2743"""
2744 result += v.get_receive()
2745 result += v.get_send()
2746 return result
2747
2748 def get_lsend(self) -> str:
2749 """Generate the implementation of the lsend function, which takes
2750 a list of connections to send a packet to."""
2751 if not self.want_lsend: return ""
2752 return """\
2753{self.lsend_prototype}
2754{{
2755 conn_list_iterate(dest, pconn) {{
2756 send_{self.name}(pconn{self.extra_send_args2});
2757 }} conn_list_iterate_end;
2758}}
2759
2760""".format(self = self)
2761
2762 def get_dsend(self) -> str:
2763 """Generate the implementation of the dsend function, which directly
2764 takes packet fields instead of a packet struct."""
2765 if not self.want_dsend: return ""
2766 fill = "".join(
2767 prefix(" ", field.get_fill())
2768 for field in self.fields
2769 )
2770 return """\
2771{self.dsend_prototype}
2772{{
2773 struct {self.name} packet, *real_packet = &packet;
2774
2775{fill}\
2776
2777 return send_{self.name}(pc, real_packet);
2778}}
2779
2780""".format(self = self, fill = fill)
2781
2782 def get_dlsend(self) -> str:
2783 """Generate the implementation of the dlsend function, combining
2784 dsend and lsend functionality.
2785
2786 See self.get_dsend() and self.get_lsend()"""
2787 if not (self.want_lsend and self.want_dsend): return ""
2788 fill = "".join(
2789 prefix(" ", field.get_fill())
2790 for field in self.fields
2791 )
2792 return """\
2793{self.dlsend_prototype}
2794{{
2795 struct {self.name} packet, *real_packet = &packet;
2796
2797{fill}\
2798
2799 lsend_{self.name}(dest, real_packet);
2800}}
2801
2802""".format(self = self, fill = fill)
2803
2804
2805class PacketsDefinition(typing.Iterable[Packet]):
2806 """Represents an entire packets definition file"""
2807
2808 COMMENT_START_PATTERN = re.compile(r"""
2809 ^\s* # strip initial whitespace
2810 (.*?) # actual content; note the reluctant quantifier
2811 \s* # note: this can cause quadratic backtracking
2812 (?: # match a potential comment
2813 (?: # EOL comment (or just EOL)
2814 (?:
2815 (?:\#|//) # opening # or //
2816 .*
2817 )?
2818 ) | (?: # block comment ~> capture remaining text
2819 /\* # opening /*
2820 [^*]* # text that definitely can't end the block comment
2821 (.*) # remaining text, might contain a closing */
2822 )
2823 )
2824 (?:\n)? # optional newline in case those aren't stripped
2825 $
2826 """, re.VERBOSE)
2827 """Used to clean lines when not starting inside a block comment. Finds
2828 the start of a block comment, if it exists.
2829
2830 Groups:
2831 - Actual content before any comment starts; stripped.
2832 - Remaining text after the start of a block comment. Not present if no
2833 block comment starts on this line."""
2834
2835 COMMENT_END_PATTERN = re.compile(r"""
2836 ^
2837 .*? # comment; note the reluctant quantifier
2838 (?: # end of block comment ~> capture remaining text
2839 \*/ # closing */
2840 \s* # strip whitespace after comment
2841 (.*) # remaining text
2842 )?
2843 (?:\n)? # optional newline in case those aren't stripped
2844 $
2845 """, re.VERBOSE)
2846 """Used to clean lines when starting inside a block comment. Finds the
2847 end of a block comment, if it exists.
2848
2849 Groups:
2850 - Remaining text after the end of the block comment; lstripped. Not
2851 present if the block comment doesn't end on this line."""
2852
2853 TYPE_PATTERN = re.compile(r"^\s*type\s+(\w+)\s*=\s*(.+?)\s*$")
2854 """Matches type alias definition lines
2855
2856 Groups:
2857 - the alias to define
2858 - the meaning for the alias"""
2859
2860 PACKET_HEADER_PATTERN = re.compile(r"^\s*(PACKET_\w+)\s*=\s*(\d+)\s*;\s*(.*?)\s*$")
2861 """Matches the header line of a packet definition
2862
2863 Groups:
2864 - packet type name
2865 - packet number
2866 - packet flags text"""
2867
2868 PACKET_END_PATTERN = re.compile(r"^\s*end\s*$")
2869 """Matches the "end" line terminating a packet definition"""
2870
2871 @classmethod
2872 def _clean_lines(cls, lines: typing.Iterable[str]) -> typing.Iterator[str]:
2873 """Strip comments and leading/trailing whitespace from the given
2874 lines. If a block comment starts in one line and ends in another,
2875 the remaining parts are joined together and yielded as one line."""
2876 inside_comment = False
2877 parts = []
2878
2879 for line in lines:
2880 while line:
2881 if inside_comment:
2882 # currently inside a block comment ~> look for */
2883 mo = cls.COMMENT_END_PATTERN.fullmatch(line)
2884 assert mo, repr(line)
2885 # If the group wasn't captured (None), we haven't found
2886 # a */ to end our comment ~> still inside_comment
2887 # Otherwise, group captured remaining line content
2888 line, = mo.groups(None)
2889 inside_comment = line is None
2890 else:
2891 mo = cls.COMMENT_START_PATTERN.fullmatch(line)
2892 assert mo, repr(line)
2893 # If the second group wasn't captured (None), there is
2894 # no /* to start a block comment ~> not inside_comment
2895 part, line = mo.groups(None)
2896 inside_comment = line is not None
2897 if part: parts.append(part)
2898
2899 if (not inside_comment) and parts:
2900 # when ending a line outside a block comment, yield what
2901 # we've accumulated
2902 yield " ".join(parts)
2903 parts.clear()
2904
2905 if inside_comment:
2906 raise ValueError("EOF while scanning block comment")
2907
2908 def parse_lines(self, lines: typing.Iterable[str]):
2909 """Parse the given lines as type and packet definitions."""
2910 self.parse_clean_lines(self._clean_lines(lines))
2911
2912 def parse_clean_lines(self, lines: typing.Iterable[str]):
2913 """Parse the given lines as type and packet definitions. Comments
2914 and blank lines must already be removed beforehand."""
2915 # hold on to the iterator itself
2916 lines_iter = iter(lines)
2917 for line in lines_iter:
2918 mo = self.TYPE_PATTERN.fullmatch(line)
2919 if mo is not None:
2920 self.define_type(*mo.groups())
2921 continue
2922
2923 mo = self.PACKET_HEADER_PATTERN.fullmatch(line)
2924 if mo is not None:
2925 packet_type, packet_number, flags_text = mo.groups("")
2926 packet_number = int(packet_number)
2927
2928 if packet_type in self.packets_by_type:
2929 raise ValueError("Duplicate packet type: " + packet_type)
2930
2931 if packet_number not in range(65536):
2932 raise ValueError("packet number %d for %s outside legal range [0,65536)" % (packet_number, packet_type))
2933 if packet_number in self.packets_by_number:
2934 raise ValueError("Duplicate packet number: %d (%s and %s)" % (
2935 packet_number,
2936 self.packets_by_number[packet_number].type,
2937 packet_type,
2938 ))
2939
2940 packet = Packet(
2941 self.cfg, packet_type, packet_number, flags_text,
2942 takewhile(
2943 lambda line: self.PACKET_END_PATTERN.fullmatch(line) is None,
2944 lines_iter, # advance the iterator used by this for loop
2945 ),
2946 self.resolve_type,
2947 )
2948
2949 self.packets.append(packet)
2950 self.packets_by_number[packet_number] = packet
2951 self.packets_by_type[packet_type] = packet
2952 self.packets_by_dirs[packet.dirs].append(packet)
2953 continue
2954
2955 raise ValueError("Unexpected line: " + line)
2956
2957 def resolve_type(self, type_text: str) -> RawFieldType:
2958 """Resolve the given type"""
2959 if type_text not in self.types:
2960 self.types[type_text] = self.type_registry.parse(type_text)
2961 return self.types[type_text]
2962
2963 def define_type(self, alias: str, meaning: str):
2964 """Define a type alias"""
2965 if alias in self.types:
2966 if meaning == self.types[alias]:
2967 self.cfg.log_verbose("duplicate typedef: %r = %r" % (alias, meaning))
2968 return
2969 else:
2970 raise ValueError("duplicate type alias %r: %r and %r"
2971 % (alias, self.types[alias], meaning))
2972
2973 self.types[alias] = self.resolve_type(meaning)
2974
2975 def __init__(self, cfg: ScriptConfig, type_registry: "TypeRegistry | None" = None):
2976 self.cfg = cfg
2977 """Configuration used for code generated from this definition"""
2978 self.type_registry = type_registry or DEFAULT_REGISTRY
2979 """Type registry used to resolve type classes for field types"""
2980 # FIXME: Once we can use Python 3.6 features, use variable
2981 # annotations instead of empty comprehensions to set element type
2982 self.types = {
2983 str(_): self.type_registry(_, _)
2984 for _ in ()
2985 }
2986 """Maps type aliases and definitions to the parsed type"""
2987 self.packets = [
2988 Packet(*[_])
2989 for _ in ()
2990 ]
2991 """List of all packets, in order of definition"""
2992 self.packets_by_type = {
2993 str(_): self.packets[_]
2994 for _ in ()
2995 }
2996 """Maps packet types (PACKET_FOO) to the packet with that type"""
2997 self.packets_by_number = {
2998 int(_): self.packets[_]
2999 for _ in ()
3000 }
3001 """Maps packet IDs to the packet with that ID"""
3002 self.packets_by_dirs = {
3003 dirs: [
3004 self.packets[_]
3005 for _ in ()
3006 ]
3007 for dirs in Directions
3008 }
3009 """Maps packet directions to lists of packets with those
3010 directions, in order of definition"""
3011
3012 def __iter__(self) -> typing.Iterator[Packet]:
3013 return iter(self.packets)
3014
3015 def iter_by_number(self) -> "typing.Generator[tuple[int, Packet, int], None, int]":
3016 """Yield (number, packet, skipped) tuples in order of packet number.
3017
3018 skipped is how many numbers were skipped since the last packet
3019
3020 Return the maximum packet number (or -1 if there are no packets)
3021 when used with `yield from`."""
3022 last = -1
3023 for n, packet in sorted(self.packets_by_number.items()):
3024 assert n == packet.type_number
3025 yield (n, packet, n - last - 1)
3026 last = n
3027 return last
3028
3029 @property
3030 def all_caps(self) -> "set[str]":
3031 """Set of all capabilities affecting the defined packets"""
3032 return set().union(*(p.all_caps for p in self))
3033
3034 @property
3035 def code_packet_functional_capability(self) -> str:
3036 """Code fragment defining the packet_functional_capability string"""
3037 return """\
3038
3039const char *const packet_functional_capability = "%s";
3040""" % " ".join(sorted(self.all_caps))
3041
3042 @property
3043 def code_delta_stats_report(self) -> str:
3044 """Code fragment implementing the delta_stats_report() function"""
3045 if not self.cfg.gen_stats: return """\
3046void delta_stats_report(void) {}
3047
3048"""
3049
3050 intro = """\
3051void delta_stats_report(void) {
3052 int i;
3053"""
3054 extro = """\
3055}
3056
3057"""
3058 body = "".join(
3059 prefix(" ", packet.get_report_part())
3060 for packet in self
3061 )
3062 return intro + body + extro
3063
3064 @property
3065 def code_delta_stats_reset(self) -> str:
3066 """Code fragment implementing the delta_stats_reset() function"""
3067 if not self.cfg.gen_stats: return """\
3068void delta_stats_reset(void) {}
3069
3070"""
3071
3072 intro = """\
3073void delta_stats_reset(void) {
3074"""
3075 extro = """\
3076}
3077
3078"""
3079 body = "\n".join(
3080 prefix(" ", packet.get_reset_part())
3081 for packet in self
3082 )
3083 return intro + body + extro
3084
3085 @property
3086 def code_packet_name(self) -> str:
3087 """Code fragment implementing the packet_name() function"""
3088 intro = """\
3089const char *packet_name(enum packet_type type)
3090{
3091 static const char *const names[PACKET_LAST] = {
3092"""
3093
3094 body = ""
3095 for _, packet, skipped in self.iter_by_number():
3096 body += """\
3097 "unknown",
3098""" * skipped
3099 body += """\
3100 "%s",
3101""" % packet.type
3102
3103 extro = """\
3104 };
3105
3106 return (type < PACKET_LAST ? names[type] : "unknown");
3107}
3108
3109"""
3110 return intro + body + extro
3111
3112 @property
3113 def code_packet_has_game_info_flag(self) -> str:
3114 """Code fragment implementing the packet_has_game_info_flag()
3115 function"""
3116 intro = """\
3117bool packet_has_game_info_flag(enum packet_type type)
3118{
3119 static const bool flag[PACKET_LAST] = {
3120"""
3121 body = ""
3122 for _, packet, skipped in self.iter_by_number():
3123 body += """\
3124 FALSE,
3125""" * skipped
3126 if packet.is_info != "game":
3127 body += """\
3128 FALSE, /* %s */
3129""" % packet.type
3130 else:
3131 body += """\
3132 TRUE, /* %s */
3133""" % packet.type
3134
3135 extro = """\
3136 };
3137
3138 return (type < PACKET_LAST ? flag[type] : FALSE);
3139}
3140
3141"""
3142 return intro + body + extro
3143
3144 @property
3145 def code_packet_handlers_fill_initial(self) -> str:
3146 """Code fragment implementing the packet_handlers_fill_initial()
3147 function"""
3148 intro = """\
3149void packet_handlers_fill_initial(struct packet_handlers *phandlers)
3150{
3151"""
3152 for cap in sorted(self.all_caps):
3153 intro += """\
3154 fc_assert_msg(has_capability("{0}", our_capability),
3155 "Packets have support for unknown '{0}' capability!");
3156""".format(cap)
3157
3158 down_only = [
3159 packet.variants[0]
3160 for packet in self.packets_by_dirs[Directions.DOWN_ONLY]
3161 if len(packet.variants) == 1
3162 ]
3163 up_only = [
3164 packet.variants[0]
3165 for packet in self.packets_by_dirs[Directions.UP_ONLY]
3166 if len(packet.variants) == 1
3167 ]
3168 unrestricted = [
3169 packet.variants[0]
3170 for packet in self.packets_by_dirs[Directions.UNRESTRICTED]
3171 if len(packet.variants) == 1
3172 ]
3173
3174 body = ""
3175 for variant in unrestricted:
3176 body += prefix(" ", variant.send_handler)
3177 body += prefix(" ", variant.receive_handler)
3178 body += """\
3179 if (is_server()) {
3180"""
3181 for variant in down_only:
3182 body += prefix(" ", variant.send_handler)
3183 for variant in up_only:
3184 body += prefix(" ", variant.receive_handler)
3185 body += """\
3186 } else {
3187"""
3188 for variant in up_only:
3189 body += prefix(" ", variant.send_handler)
3190 for variant in down_only:
3191 body += prefix(" ", variant.receive_handler)
3192
3193 extro = """\
3194 }
3195}
3196
3197"""
3198 return intro + body + extro
3199
3200 @property
3201 def code_packet_handlers_fill_capability(self) -> str:
3202 """Code fragment implementing the packet_handlers_fill_capability()
3203 function"""
3204 intro = """\
3205void packet_handlers_fill_capability(struct packet_handlers *phandlers,
3206 const char *capability)
3207{
3208"""
3209
3210 down_only = [
3211 packet
3212 for packet in self.packets_by_dirs[Directions.DOWN_ONLY]
3213 if len(packet.variants) > 1
3214 ]
3215 up_only = [
3216 packet
3217 for packet in self.packets_by_dirs[Directions.UP_ONLY]
3218 if len(packet.variants) > 1
3219 ]
3220 unrestricted = [
3221 packet
3222 for packet in self.packets_by_dirs[Directions.UNRESTRICTED]
3223 if len(packet.variants) > 1
3224 ]
3225
3226 body = ""
3227 for p in unrestricted:
3228 body += " "
3229 for v in p.variants:
3230 hand = prefix(" ", v.send_handler + v.receive_handler)
3231 body += """if ({v.condition}) {{
3232 {v.log_macro}("{v.type}: using variant={v.no} cap=%s", capability);
3233{hand}\
3234 }} else """.format(v = v, hand = hand)
3235 body += """{{
3236 log_error("Unknown {p.type} variant for cap %s", capability);
3237 }}
3238""".format(p = p)
3239 if up_only or down_only:
3240 body += """\
3241 if (is_server()) {
3242"""
3243 for p in down_only:
3244 body += " "
3245 for v in p.variants:
3246 hand = prefix(" ", v.send_handler)
3247 body += """if ({v.condition}) {{
3248 {v.log_macro}("{v.type}: using variant={v.no} cap=%s", capability);
3249{hand}\
3250 }} else """.format(v = v, hand = hand)
3251 body += """{{
3252 log_error("Unknown {p.type} variant for cap %s", capability);
3253 }}
3254""".format(p = p)
3255 for p in up_only:
3256 body += " "
3257 for v in p.variants:
3258 hand = prefix(" ", v.receive_handler)
3259 body += """if ({v.condition}) {{
3260 {v.log_macro}("{v.type}: using variant={v.no} cap=%s", capability);
3261{hand}\
3262 }} else """.format(v = v, hand = hand)
3263 body += """{{
3264 log_error("Unknown {p.type} variant for cap %s", capability);
3265 }}
3266""".format(p = p)
3267 body += """\
3268 } else {
3269"""
3270 for p in up_only:
3271 body += " "
3272 for v in p.variants:
3273 hand = prefix(" ", v.send_handler)
3274 body += """if ({v.condition}) {{
3275 {v.log_macro}("{v.type}: using variant={v.no} cap=%s", capability);
3276{hand}\
3277 }} else """.format(v = v, hand = hand)
3278 body += """{{
3279 log_error("Unknown {p.type} variant for cap %s", capability);
3280 }}
3281""".format(p = p)
3282 for p in down_only:
3283 body += " "
3284 for v in p.variants:
3285 hand = prefix(" ", v.receive_handler)
3286 body += """if ({v.condition}) {{
3287 {v.log_macro}("{v.type}: using variant={v.no} cap=%s", capability);
3288{hand}\
3289 }} else """.format(v = v, hand = hand)
3290 body += """{{
3291 log_error("Unknown {p.type} variant for cap %s", capability);
3292 }}
3293""".format(p = p)
3294 body += """\
3295 }
3296"""
3297
3298 extro = """\
3299}
3300"""
3301 return intro + body + extro
3302
3303 @property
3304 def code_enum_packet(self) -> str:
3305 """Code fragment declaring the packet_type enum"""
3306 intro = """\
3307enum packet_type {
3308"""
3309 body = ""
3310 for n, packet, skipped in self.iter_by_number():
3311 if skipped:
3312 line = " %s = %d," % (packet.type, n)
3313 else:
3314 line = " %s," % (packet.type)
3315
3316 if not (n % 10):
3317 line = "%-40s /* %d */" % (line, n)
3318 body += line + "\n"
3319
3320 extro = """\
3321
3322 PACKET_LAST /* leave this last */
3323};
3324
3325"""
3326 return intro + body + extro
3327
3328
3329########################### Writing output files ###########################
3330
3331def write_common_header(path: "str | Path | None", packets: PacketsDefinition):
3332 """Write contents for common/packets_gen.h to the given path"""
3333 if path is None:
3334 return
3335 with packets.cfg.open_write(path, wrap_header = "packets_gen") as output_h:
3336 output_h.write("""\
3337/* common */
3338#include "actions.h"
3339#include "city.h"
3340#include "disaster.h"
3341#include "unit.h"
3342
3343/* common/aicore */
3344#include "cm.h"
3345
3346""")
3347
3348 # write structs
3349 for p in packets:
3350 output_h.write(p.get_struct())
3351
3352 output_h.write(packets.code_enum_packet)
3353
3354 # write function prototypes
3355 for p in packets:
3356 output_h.write(p.get_prototypes())
3357 output_h.write("""\
3358void delta_stats_report(void);
3359void delta_stats_reset(void);
3360""")
3361
3362def write_common_impl(path: "str | Path | None", packets: PacketsDefinition):
3363 """Write contents for common/packets_gen.c to the given path"""
3364 if path is None:
3365 return
3366 with packets.cfg.open_write(path) as output_c:
3367 output_c.write("""\
3368#ifdef HAVE_CONFIG_H
3369#include <fc_config.h>
3370#endif
3371
3372#include <string.h>
3373
3374/* utility */
3375#include "bitvector.h"
3376#include "capability.h"
3377#include "genhash.h"
3378#include "log.h"
3379#include "mem.h"
3380#include "support.h"
3381
3382/* common */
3383#include "capstr.h"
3384#include "connection.h"
3385#include "dataio.h"
3386#include "game.h"
3387
3388#include "packets.h"
3389""")
3390 output_c.write(packets.code_packet_functional_capability)
3391 output_c.write("""\
3392
3393#ifdef FREECIV_DELTA_PROTOCOL
3394static genhash_val_t hash_const(const void *vkey)
3395{
3396 return 0;
3397}
3398
3399static bool cmp_const(const void *vkey1, const void *vkey2)
3400{
3401 return TRUE;
3402}
3403#endif /* FREECIV_DELTA_PROTOCOL */
3404
3405""")
3406
3407 if packets.cfg.gen_stats:
3408 output_c.write("""\
3409static int stats_total_sent;
3410
3411""")
3412 # write stats
3413 for p in packets:
3414 output_c.write(p.get_stats())
3415 # write report()
3416 output_c.write(packets.code_delta_stats_report)
3417 output_c.write(packets.code_delta_stats_reset)
3418
3419 output_c.write(packets.code_packet_name)
3420 output_c.write(packets.code_packet_has_game_info_flag)
3421
3422 # write hash, cmp, send, receive
3423 for p in packets:
3424 output_c.write(p.get_variants())
3425 output_c.write(p.get_send())
3426 output_c.write(p.get_lsend())
3427 output_c.write(p.get_dsend())
3428 output_c.write(p.get_dlsend())
3429
3430 output_c.write(packets.code_packet_handlers_fill_initial)
3431 output_c.write(packets.code_packet_handlers_fill_capability)
3432
3433def write_server_header(path: "str | Path | None", packets: PacketsDefinition):
3434 """Write contents for server/hand_gen.h to the given path"""
3435 if path is None:
3436 return
3437 with packets.cfg.open_write(path, wrap_header = "hand_gen", cplusplus = False) as f:
3438 f.write("""\
3439/* utility */
3440#include "shared.h"
3441
3442/* common */
3443#include "fc_types.h"
3444#include "packets.h"
3445
3446struct connection;
3447
3448bool server_handle_packet(enum packet_type type, const void *packet,
3449 struct player *pplayer, struct connection *pconn);
3450
3451""")
3452
3453 for p in packets:
3454 if p.dirs.up and not p.no_handle:
3455 a=p.name[len("packet_"):]
3456 b = "".join(
3457 ", %s" % field.get_handle_param()
3458 for field in p.fields
3459 )
3460 if p.handle_per_conn:
3461 sender = "struct connection *pc"
3462 else:
3463 sender = "struct player *pplayer"
3464 if p.handle_via_packet:
3465 f.write("""\
3466struct %s;
3467void handle_%s(%s, const struct %s *packet);
3468""" % (p.name, a, sender, p.name))
3469 else:
3470 f.write("""\
3471void handle_%s(%s%s);
3472""" % (a, sender, b))
3473
3474def write_client_header(path: "str | Path | None", packets: PacketsDefinition):
3475 """Write contents for client/packhand_gen.h to the given path"""
3476 if path is None:
3477 return
3478 with packets.cfg.open_write(path, wrap_header = "packhand_gen") as f:
3479 f.write("""\
3480/* utility */
3481#include "shared.h"
3482
3483/* common */
3484#include "packets.h"
3485
3486bool client_handle_packet(enum packet_type type, const void *packet);
3487
3488""")
3489 for p in packets:
3490 if not p.dirs.down: continue
3491
3492 a=p.name[len("packet_"):]
3493 b = ", ".join(
3494 field.get_handle_param()
3495 for field in p.fields
3496 ) or "void"
3497 if p.handle_via_packet:
3498 f.write("""\
3499struct %s;
3500void handle_%s(const struct %s *packet);
3501""" % (p.name, a, p.name))
3502 else:
3503 f.write("""\
3504void handle_%s(%s);
3505""" % (a, b))
3506
3507def write_server_impl(path: "str | Path | None", packets: PacketsDefinition):
3508 """Write contents for server/hand_gen.c to the given path"""
3509 if path is None:
3510 return
3511 with packets.cfg.open_write(path) as f:
3512 f.write("""\
3513#ifdef HAVE_CONFIG_H
3514#include <fc_config.h>
3515#endif
3516
3517/* common */
3518#include "packets.h"
3519
3520#include "hand_gen.h"
3521
3522bool server_handle_packet(enum packet_type type, const void *packet,
3523 struct player *pplayer, struct connection *pconn)
3524{
3525 switch (type) {
3526""")
3527 for p in packets:
3528 if not p.dirs.up: continue
3529 if p.no_handle: continue
3530 a=p.name[len("packet_"):]
3531
3532 if p.handle_via_packet:
3533 args = ", packet"
3534
3535 else:
3536 packet_arrow = "((const struct %s *)packet)->" % p.name
3537 args = "".join(
3538 ",\n " + field.get_handle_arg(packet_arrow)
3539 for field in p.fields
3540 )
3541
3542 if p.handle_per_conn:
3543 first_arg = "pconn"
3544 else:
3545 first_arg = "pplayer"
3546
3547 f.write("""\
3548 case %s:
3549 handle_%s(%s%s);
3550 return TRUE;
3551
3552""" % (p.type, a, first_arg, args))
3553 f.write("""\
3554 default:
3555 return FALSE;
3556 }
3557}
3558""")
3559
3560def write_client_impl(path: "str | Path | None", packets: PacketsDefinition):
3561 """Write contents for client/packhand_gen.c to the given path"""
3562 if path is None:
3563 return
3564 with packets.cfg.open_write(path) as f:
3565 f.write("""\
3566#ifdef HAVE_CONFIG_H
3567#include <fc_config.h>
3568#endif
3569
3570/* common */
3571#include "packets.h"
3572
3573#include "packhand_gen.h"
3574
3575bool client_handle_packet(enum packet_type type, const void *packet)
3576{
3577 switch (type) {
3578""")
3579 for p in packets:
3580 if not p.dirs.down: continue
3581 if p.no_handle: continue
3582 a=p.name[len("packet_"):]
3583
3584 if p.handle_via_packet:
3585 args="packet"
3586 else:
3587 packet_arrow = "((const struct %s *)packet)->" % p.name
3588 args = "".join(
3589 ",\n " + field.get_handle_arg(packet_arrow)
3590 for field in p.fields
3591 )[1:] # cut off initial comma
3592
3593 f.write("""\
3594 case %s:
3595 handle_%s(%s);
3596 return TRUE;
3597
3598""" % (p.type, a, args))
3599 f.write("""\
3600 default:
3601 return FALSE;
3602 }
3603}
3604""")
3605
3606
3607def main(raw_args: "typing.Sequence[str] | None" = None):
3608 """Main function. Read the given arguments, or the command line
3609 arguments if raw_args is not given, and run the packet code generation
3610 script accordingly."""
3611 script_args = ScriptConfig(raw_args)
3612
3613 packets = PacketsDefinition(script_args)
3614 for path in script_args.def_paths:
3615 with path.open() as input_file:
3616 packets.parse_lines(input_file)
3617
3618 write_common_header(script_args.common_header_path, packets)
3619 write_common_impl(script_args.common_impl_path, packets)
3620 write_server_header(script_args.server_header_path, packets)
3621 write_client_header(script_args.client_header_path, packets)
3622 write_server_impl(script_args.server_impl_path, packets)
3623 write_client_impl(script_args.client_impl_path, packets)
3624
3625
3626if __name__ == "__main__":
3627 main()
generate_packets.ScriptConfig
Definition generate_packets.py:54
generate_packets.ScriptConfig.__init__
__init__(self, "typing.Sequence[str] | None" args=None)
Definition generate_packets.py:146
generate_packets.ScriptConfig.get_argparser
argparse.ArgumentParser get_argparser()
Definition generate_packets.py:59
generate_packets.ScriptConfig.def_paths
def_paths
Definition generate_packets.py:151
incite_cost
char * incite_cost
Definition comments.c:74
generate_packets.file_path
Path file_path("str | Path" s)
Parsing Command Line Arguments ######################.
Definition generate_packets.py:42