Analysis: Other Game/SDK Extension

I've been poking around in other projects and I think we can safely eliminate C-Evo:
  • It's written in Delphi, an obsolete variant of Pascal that I known nothing about. Only the AI supports C++ and C#.
  • Not natively cross-platform
  • The code is in the author's own words "is not written with the principle to allow more than one person to work on it. Some parts might seem easy to understand, but there are several undocumented dependencies and other critical mechanisms, that need to be known in order to avoid bugs of strange and hard to reproduce kind."
 
Those are some scary words from the author! That probably makes it harder to gain collaborators. But at least the author is upfront about the quality of the code.

I haven't written any Pascal either. I recall it used to be popular in education back in the day (i.e. the 1980s), but haven't run across it myself.
 
I've used Delphi and Pascal, but my C++ experience 26+ years ago is much more recent!

Delphi is basically "Visual Pascal" in the way that VB came out of BASIC. I think I actually owned it once but didn't really do anything with it.

Wait, actually I *did* touch Pascal a year or two ago...I still have some code I made on the Apple //e and was able to get it going in an emulator. (Not kidding, this is all true.)

Pascal is *not* in the "top" lists of popularity in several decades.
 
This week I've been browsing the source of Call to Power 2 (C & C++ with DirectX). Fun fact, it actually refers to itself as Civ3 in the code, presumably a carryover from when CivCtP had the rights to the franchise.

I haven't had any particular revelations from this code one way or another, but doing it has made me rethink the practicality of this whole approach. It's the least discussed option so far. The idea is that starting from a working game gives us a lot of progress for free, and that it's a trade-off of less overall work for less technical freedom.

Does it actually save us work though? That's the bottom line, if not there's no reason to pursue it. And the more I think about it the less likely that seems.

Say we use CtP2 as our baseline. Like the other remaining candidates (ignoring IndieCiv for the moment; that's an outlier in many ways) it's old C/C++ code, so right away that has implications for productivity. All else being equal it tends to take more effort to implement a feature in C++ than C#, plus we all seem to favor the latter. It's bespoke code with no engine to speak of, so there's no leveraging an off-the-shelf framework to make things easier. Again, less productive.

Ok, so it's going to be harder work, but how much of it is already done for us? Maybe not all that much. We've established that we want something not just similar to but compatible with Civ3, including graphics/UI, gameplay, and in some way loading mods. So we're going to first have to strip out or rewrite everything that's not functionally equivalent to Civ3: the UI, any file handling, graphics formats, data structures, most of the game rules, not to mention outdated APIs... In other words everything that makes it that other game.

What's left is the architecture and core turn based 4X game logic. Well that's the easy stuff! It could very well take as much work just to pare down the code to the common features as it would to implement that part from scratch. And then we have all the rest of the game to put back together without the help of a proper engine.

The same general points apply to FreeCiv and the Civ4 SDK, except that Civ4 is a bit newer and is based on the Gamebryo engine. I'm just not seeing it being a net benefit anymore.

Thoughts? Counterpoints?
 
Concerning creating a New vs using an existing Game Engine... For me, the "Jury is out" deliberating the pros and cons, trying to first decide on what criteria is most important to then be able to collectively decide on what will provide the best way to go.

I believe we all know the basics of what is wanted, therefore Questions such as what will ultimately provide the Best results to gain what is wanted vs time and effort to gain it.

Logic tells me that it would be easier to create a New Game Engine because it would offer a New, Fresh approach that is better understood by the Creators (basically the Programmers) as it is developed. That said, as has been pointed out, if an existing Game Engine can offer not only an easier start but Good end results, why not use it? It all really gets back to time and level of difficulty coupled with the bottom line of gaining the desired end results.

The Group members that can best answer the question of creating a New vs Existing Game Engine are you Programmers because you will be doing the difficult and time consuming work to develop the Game Engine.

If there are other Programmers you guys know of that might be interested in this endeavor, they could help "lighten the load" for you Programmers working on a Game Engine and expedite its creation.

Programming is Creation and All Creation stems from knowledge and experience put together in a different way to create something New. This is where having others with experience that can help in needed areas is extremely beneficial.

New Creations are not started due to fear of failure and self doubt that prevents attempting something that has not been done.

As Henry Ford said, "Whether you think you can or you think you can't, you're right."

I say Feel the Fear and do it anyway because "nothing ventured, nothing gained."
 
I took a look at some of the Rulesets that @Ozymandias mentioned. It looks to me like rulesets it make it relatively easy to tweak the rules of the game, in essence setting up different scenarios. For example, you can create custom unit classes, beyond the Air/Land/Sea that Civ3 has. Unit classes are kind of a mash-up of Civ3 unit attributes (zone of control, whether they can fortify, if it's a missile, etc.), but also have the Air/Land/Sea/Both movement option. The rulesets themselves, however, appear to be more akin to a BIQ, but in human-readable text format, than something we could quickly modify to make FreeCiv more CivIII-like.

Put another way, within the confines of the overlap between what features FreeCiv supports and what Civ III supports, we could likely tweak the "civ2civ3" ruleset to be more like Civ III relatively easily. But where there are Civ3 features that aren't supported in an existing ruleset, we'd have to implement it in code, which is where we run into the "400,000 lines of C" problem. I'll follow on that shortly, but first wanted to note that it may be worth investigating what the gap is between civ2civ3 in FreeCiv, and what could be supported if that ruleset were fully enhanced. I don't expect full compatibility with scenarios, but it may be closer than what it is now. I also believe @WildWeazel has looked at civ2civ3 some in the past.

So what does a ruleset look like in implementation? I looked at the "CanOccupyCity" unit rule, as a random example within the unit rulesets that had a long enough name to not match everywhere in the code base. It shows up 7 times in the C code, which is reasonable enough. A couple example; the first is from the "do_paradrop" method, which is about 100 lines overall:

Code:
  if (is_non_attack_city_tile(ptile, pplayer)
      || (is_non_allied_city_tile(ptile, pplayer)
          && (pplayer->ai_common.barbarian_type == ANIMAL_BARBARIAN
              || !uclass_has_flag(unit_class_get(punit),
                               UCF_CAN_OCCUPY_CITY)
              || unit_has_type_flag(punit, UTYF_CIVILIAN)))
      || is_non_allied_unit_tile(ptile, pplayer)) {
    map_show_circle(pplayer, ptile, unit_type_get(punit)->vision_radius_sq);
    maybe_make_contact(ptile, pplayer);
    notify_player(pplayer, ptile, E_UNIT_LOST_MISC, ftc_server,
                  _("Your %s was killed by enemy units at the "
                    "paradrop destination."),
                  unit_tile_link(punit));
    /* TODO: Should defender score.units_killed get increased too?
     * What if there's units of several allied players? Should the
     * city owner or owner of the first/random unit get the kill? */
    pplayer->score.units_lost++;
    server_remove_unit(punit, ULR_KILLED);
    return TRUE;
  }

It's part of a 7-line conditional statement that, if true, resolves the paradrop action. This is preceded by three other conditionals; they are in order the "cannot paradrop there", "you aren't at war and can't paradrop", "your unit paradropped and was lost", and (this one) "your unit was killed by enemy units at the paradrop destination" cases. It's only because of the text in the notify_player function arguments that I was able to figure out in any reasonable amount of time what this block of code is trying to do. A 7-line-long conditional is a terrifying thing; this should have been delegated to a function that took ptile and pplayer as arguments, had a name such as "isUnitKilledAtParadropDestination", and returned true or false. But the gist of it is, whether a unit can occupy a city impacts its paradrop ability.

Another example where this flag is used is in the unit move code:

Code:
/*************************************************************************//**
  Move a unit.
*****************************************************************************/
bool api_edit_unit_move_old(lua_State *L, Unit *punit, Tile *ptile,
                            int movecost)
{
  struct city *pcity;

  deprecated_semantic_warning("edit.unit_move(unit, moveto, movecost)",
                              "Unit:move(moveto, movecost)", "3.1");

  LUASCRIPT_CHECK_STATE(L, FALSE);
  LUASCRIPT_CHECK_SELF(L, punit, FALSE);
  LUASCRIPT_CHECK_ARG_NIL(L, ptile, 3, Tile, FALSE);
  LUASCRIPT_CHECK_ARG(L, movecost >= 0, 4, "Negative move cost!", FALSE);

  return unit_move(punit, ptile, movecost,
                   /* Auto embark kept for backward compatibility. I have
                    * no objection if you see the old behavior as a bug and
                    * remove auto embarking completely or for transports
                    * the unit can't legally board. -- Sveinung */
                   NULL, TRUE,
                   /* Backwards compatibility for old scripts in rulesets
                    * and (scenario) savegames. I have no objection if you
                    * see the old behavior as a bug and remove auto
                    * conquering completely or for cities the unit can't
                    * legally conquer. -- Sveinung */
                   ((pcity = tile_city(ptile))
                    && (unit_owner(punit)->ai_common.barbarian_type
                        != ANIMAL_BARBARIAN)
                    && uclass_has_flag(unit_class_get(punit),
                                       UCF_CAN_OCCUPY_CITY)
                    && !unit_has_type_flag(punit, UTYF_CIVILIAN)
                    && pplayers_at_war(unit_owner(punit),
                                       city_owner(pcity))),
                   (extra_owner(ptile) == NULL
                    || pplayers_at_war(extra_owner(ptile),
                                       unit_owner(punit)))
                   && tile_has_claimable_base(ptile, unit_type_get(punit)),
                   /* Backwards compatibility: unit_enter_hut() would
                    * return without doing anything if the unit was
                    * HUT_NOTHING. Setting this parameter to FALSE makes
                    * sure unit_enter_hut() isn't called. */
                   unit_can_do_action_result(punit, ACTRES_HUT_ENTER),
                   unit_can_do_action_result(punit, ACTRES_HUT_FRIGHTEN));
}

We see some Lua invocations, which is cool. But the thing that jumps out to me is that unit_move invocation. How many arguments is that? Let's look at unit_move:

Code:
/**********************************************************************//**
  Moves a unit. No checks whatsoever! This is meant as a practical
  function for other functions, like do_airline, which do the checking
  themselves.

  If you move a unit you should always use this function, as it also sets
  the transport status of the unit correctly. Note that the source tile (the
  current tile of the unit) and pdesttile need not be adjacent.

  Returns TRUE iff unit still alive.
**************************************************************************/
bool unit_move(struct unit *punit, struct tile *pdesttile, int move_cost,
               struct unit *embark_to, bool find_embark_target,
               bool conquer_city_allowed, bool conquer_extras_allowed,
               bool enter_hut, bool frighten_hut)

9 arguments. It's clear enough why *punit and *pdesttile are necessary, but why do we need to pass conquer_city_allowed instead of looking at data on the *punit struct and figuring that out ourselves (and only in the case that the unit would be conquering a city)? unit_move is itself a 400-line method, so it's also a lot to handle. [This is in essence an argument that the code is not sufficiently OO; while C is not an OO language, you can get closer than what we see here]

Now perhaps it's a low-level nuts-and-bots method most of FreeCiv's modding is at a higher level. Let's look at the do_airline method that the comment above referenced:

Code:
/**********************************************************************//**
  Go by airline, if both cities have an airport and neither has been used this
  turn the unit will be transported by it and have its moves set to 0
**************************************************************************/
bool do_airline(struct unit *punit, struct city *pdest_city,
                const struct action *paction)
{
  struct city *psrc_city = tile_city(unit_tile(punit));

  notify_player(unit_owner(punit), city_tile(pdest_city),
                E_UNIT_RELOCATED, ftc_server,
                _("%s transported successfully."),
                unit_link(punit));

  unit_move(punit, pdest_city->tile, punit->moves_left,
            NULL, BV_ISSET(paction->sub_results, ACT_SUB_RES_MAY_EMBARK),
            /* Can only airlift to allied and domestic cities */
            FALSE, FALSE,
            BV_ISSET(paction->sub_results, ACT_SUB_RES_HUT_ENTER),
            BV_ISSET(paction->sub_results, ACT_SUB_RES_HUT_FRIGHTEN));

  /* Update airlift fields. */
  if (!(game.info.airlifting_style & AIRLIFTING_UNLIMITED_SRC)) {
    psrc_city->airlift--;
    send_city_info(city_owner(psrc_city), psrc_city);
  }
  if (!(game.info.airlifting_style & AIRLIFTING_UNLIMITED_DEST)) {
    pdest_city->airlift--;
    send_city_info(city_owner(pdest_city), pdest_city);
  }

  return TRUE;
}

This is more reasonable, and is fairly typical of the various types of moves - conquer extra, disembark, unit hut, embark, conquer city, regular move, ignore zone of control, teleport, bounce, and paradrop to name most of them.

My thoughts in general:
  • While there are a lot of comments, some aspects of the code make it brittle and harder to understand. Why do we have conditional checks spread all over the code base to make sure we only call unit_move when it is valid, instead of having that be the first thing we check in that method, or at least having a can_make_move method? Why have methods been allowed to grow to 9 arguments instead of being refactored.
  • We would probably have to get down to the low level to add new features that FreeCiv doesn't support. The appeal of jumping into a 400-line method that takes 9 arguments and does no checks whatsoever is not great. It would be easy to make mistakes there that impact other areas.
  • If anything, the code errs on the side of being too low level, which might be better from an "ability to jump in" perspective than if it used as much advanced C++ as it could. As much as C# would be nice over C or C++, I'd probably rather dive into a C codebase than C++ (although this is in part because I've written C in the past couple years, but not C++; that may be the opposite for other members of this forum).
  • If we want to explore this approach more, it would probably make sense to start with one very small feature that Civ III has, but FreeCiv doesn't, and see how implementing that goes. A simultaneous study of the rulesets and just how wide the gulf is between FreeCiv and Civ III by non-coding members could combine with this to get an idea of, "if it took X time to add this small features and we have Y features (some of which are not small), what is the outlook?" The very small feature would also test whether there is any appetite from a coding standpoint to proceed in an old C codebase (perhaps influenced by the amount of features required for compatibility... it could be "that was painful but if we only need 12 more things, it makes sense; if we need 200, it doesn't").
I'd be curious how that compares with what WildWeazel has found in Call to Power 2. It does look better than C-Evo; the amount of comments indicates some effort to make it consumable by multiple people, and I was able to trace through the code without becoming completely lost
 
Last edited:
I'd probably rather dive into a C codebase than C++
Hehe, that's my preference, too... There is an old saying, that describes it pretty well:

"With C, you can easily shoot yourself in the foot. With C++, you can accidentally create 10.000 copies of yourself and shoot all of them in the foot..."

I would be willing to dig into the C++ code-base, if Ozymandias can really get his hands on the original game code, but in all other cases, C# would be my preferred choice.
 
Top Bottom