Excruciating FOCS doubts

Creation, discussion, and balancing of game content such as techs, buildings, ship parts.

Moderators: Oberlus, Committer, Oberlus, Committer

Post Reply
Message
Author
User avatar
Oberlus
Cosmic Dragon
Posts: 1796
Joined: Mon Apr 10, 2017 4:25 pm

Excruciating FOCS doubts

#1 Post by Oberlus » Thu Nov 21, 2019 10:54 am

I'm trying to understand FOCS, to generate my own code, rather than being subject to mimic existing pieces of code without knowing why a previous attempt didn't work or how and why this other piece of code does work.
I've searched for a general thread of FOCS doubts and couldn't find one. I've found specific doubts on dedicated (short) threads, but here I'm not trying to solve a specific doubt, I'm just browsing FOCS files at random, watching their code, and trying to understand each pieace of code. So I thought a general FOCS doubts threads makes sense.

So I'll be posting doubts here. However, if you think it would be better to have a thread for each doubt, I'll split the thread as needed.

User avatar
Oberlus
Cosmic Dragon
Posts: 1796
Joined: Mon Apr 10, 2017 4:25 pm

Re: Excruciating FOCS doubts

#2 Post by Oberlus » Thu Nov 21, 2019 11:21 am

SP_KRILL_SPAWNER.focs.txt is a ship part (category General), mountable in internal slots.

I don't understant its first EffectsGroup:

Code: Select all

Part
    name = "SP_KRILL_SPAWNER"
    description = "SP_KRILL_SPAWNER_DESC"
    class = General
    mountableSlotTypes = Internal
    buildcost = 100 * [[FLEET_UPKEEP_MULTIPLICATOR]] * [[SHIP_PART_COST_MULTIPLIER]]
    buildtime = 9
    tags = [ "PEDIA_PC_GENERAL" ]
    location = All
    effectsgroups = [
        EffectsGroup
            scope = And [
                Planet
                Planet type = Asteroids
                InSystem id = Source.SystemID
                Unowned
            ]
            activation = And [
                InSystem
                Not ContainedBy And [
                    Object id = Source.SystemID
                    System
                    Contains And [
                        Ship
                        InSystem id = Source.SystemID
                        Or [
                            DesignHasHull name = "SH_KRILL_1_BODY"
                            DesignHasHull name = "SH_KRILL_2_BODY"
                            DesignHasHull name = "SH_KRILL_3_BODY"
                            DesignHasHull name = "SH_KRILL_4_BODY"
                        ]
                    ]
                ]
            ]
            stackinggroup = "SP_KRILL_SPAWN_STACK"
            effects = CreateShip designname = "SM_KRILL_1"

        EffectsGroup
            scope = Source
            activation = And [
                            Not DesignHasPartClass Low=1 High=999 Class=Stealth
                            Not Armed
                        ]
            effects = SetStealth value = Value + 40
    ]
First of all, Source is the ship part SP_KRILL_SPAWNER itself, right?

Scope:
  • Any planet
  • of type Asteroids
  • that is in the system of the Source
  • and that is unowned
So this applies to asteroid belts in the same system than the ship mounting the SP_KRILL_SPAWNER, right?

Activation:
  • InSystem? That is not travelling a starlane? But isn't now the Target an unowned asteroid belt that should not be travelling? Or maybe this is asking for the Target to be in the same system than Source? Or just this is asking for a System, in general?
  • That is NOT contained by
    • an object that is the System in which the SP_KRILL_SPAWNER was?
    • which is itself a system (shouldn't this be implicit from previous line?)
    • and that contains
      • a ship
      • that is in the system of the SP_KRILL_SPAWNER?
      • and that has a krill body hull
So it creates a monster SM_KRILL_1 in... any place that is not the system of the SP_KRILL_SPAWNER as long as it does not have a SM_KRILL_X in there? Obviously that's not true, so I certainly can't figure out what the heck means all those lines in the conditions of the scope and activation.

Please, a charitable bright mind that helps me out here?

"InSystem" is a keyword not referenced in https://www.freeorion.org/index.php/FOC ... ng_Details nor in https://www.freeorion.org/index.php/FOC ... g_Tutorial
One of the problems with FOCS is that you can't relly on the documentation. You certainly can't go to StackOverflow to ask about it or browse the latest version of the specification.

User avatar
alleryn
Space Dragon
Posts: 259
Joined: Sun Nov 19, 2017 6:32 pm

Re: Excruciating FOCS doubts

#3 Post by alleryn » Thu Nov 21, 2019 4:10 pm

First, I'll try to help with the general lexical process i use when i try to decipher FOCS. I'll use as an example InSystem:
  1. Go to the FreeOrion repository. https://github.com/freeorion/freeorion/tree/master
  2. In the search box at the upper right look for the term you are trying to decipher: InSystem
  3. Look through the search result for a file name with "parser" in the title. In this case, on page 4 there is a result in ConditionParser2.cpp
  4. Open up the link to that file, and do a web page search for InSystem. Here you can locate the code snippet:

    Code: Select all

            in_system
                =   (   omit_[tok.InSystem_]
                        >  -(label(tok.ID_)  > int_rules.expr)
                    ) [ _val = construct_movable_(new_<Condition::InSystem>(deconstruct_movable_(_1, _pass))) ]
                ;
    This shows that InSystem (after being tokenized into tok.InSystem_ -- if you want to check this part, it takes a bit more digging, but this tok.<focs>_ construction is common) gets converted into Condition::InSystem.

    Now we can go to Condition.cpp and look for Condition::InSystem to find:

    Code: Select all

    ///////////////////////////////////////////////////////////
    // InSystem                                              //
    ///////////////////////////////////////////////////////////
    InSystem::InSystem(std::unique_ptr<ValueRef::ValueRefBase<int>>&& system_id) :
        ConditionBase(),
        m_system_id(std::move(system_id))
    {}
    
    bool InSystem::operator==(const ConditionBase& rhs) const {
        if (this == &rhs)
            return true;
        if (typeid(*this) != typeid(rhs))
            return false;
    
        const InSystem& rhs_ = static_cast<const InSystem&>(rhs);
    
        CHECK_COND_VREF_MEMBER(m_system_id)
    
        return true;
    }
    
    namespace {
        struct InSystemSimpleMatch {
            InSystemSimpleMatch(int system_id) :
                m_system_id(system_id)
            {}
    
            bool operator()(std::shared_ptr<const UniverseObject> candidate) const {
                if (!candidate)
                    return false;
                if (m_system_id == INVALID_OBJECT_ID)
                    return candidate->SystemID() != INVALID_OBJECT_ID;  // match objects in any system
                else
                    return candidate->SystemID() == m_system_id;        // match objects in specified system
            }
    
            int m_system_id;
        };
    }
    
    void InSystem::Eval(const ScriptingContext& parent_context,
                        ObjectSet& matches, ObjectSet& non_matches,
                        SearchDomain search_domain/* = NON_MATCHES*/) const
    {
        bool simple_eval_safe = !m_system_id || m_system_id->ConstantExpr() ||
                                (m_system_id->LocalCandidateInvariant() &&
                                (parent_context.condition_root_candidate || RootCandidateInvariant()));
        if (simple_eval_safe) {
            // evaluate empire id once, and use to check all candidate objects
            std::shared_ptr<const UniverseObject> no_object;
            int system_id = (m_system_id ? m_system_id->Eval(ScriptingContext(parent_context, no_object)) : INVALID_OBJECT_ID);
            EvalImpl(matches, non_matches, search_domain, InSystemSimpleMatch(system_id));
        } else {
            // re-evaluate empire id for each candidate object
            ConditionBase::Eval(parent_context, matches, non_matches, search_domain);
        }
    }
    
    bool InSystem::RootCandidateInvariant() const
    { return !m_system_id || m_system_id->RootCandidateInvariant(); }
    
    bool InSystem::TargetInvariant() const
    { return !m_system_id || m_system_id->TargetInvariant(); }
    
    bool InSystem::SourceInvariant() const
    { return !m_system_id || m_system_id->SourceInvariant(); }
    
    std::string InSystem::Description(bool negated/* = false*/) const {
        std::string system_str;
        int system_id = INVALID_OBJECT_ID;
        if (m_system_id && m_system_id->ConstantExpr())
            system_id = m_system_id->Eval();
        if (auto system = GetSystem(system_id))
            system_str = system->Name();
        else if (m_system_id)
            system_str = m_system_id->Description();
    
        std::string description_str;
        if (!system_str.empty())
            description_str = (!negated)
                ? UserString("DESC_IN_SYSTEM")
                : UserString("DESC_IN_SYSTEM_NOT");
        else
            description_str = (!negated)
                ? UserString("DESC_IN_SYSTEM_SIMPLE")
                : UserString("DESC_IN_SYSTEM_SIMPLE_NOT");
    
        return str(FlexibleFormat(description_str) % system_str);
    }
    
    std::string InSystem::Dump(unsigned short ntabs) const {
        std::string retval = DumpIndent(ntabs) + "InSystem";
        if (m_system_id)
            retval += " id = " + m_system_id->Dump(ntabs);
        retval += "\n";
        return retval;
    }
    
    void InSystem::GetDefaultInitialCandidateObjects(const ScriptingContext& parent_context,
                                                     ObjectSet& condition_non_targets) const
    {
        if (!m_system_id) {
            // can match objects in any system, or any system
            AddAllObjectsSet(condition_non_targets);
            return;
        }
    
        bool simple_eval_safe = m_system_id->ConstantExpr() ||
                                (m_system_id->LocalCandidateInvariant() &&
                                (parent_context.condition_root_candidate || RootCandidateInvariant()));
    
        if (!simple_eval_safe) {
            // almost anything can be in a system, and can also match the system itself
            AddAllObjectsSet(condition_non_targets);
            return;
        }
    
        // simple case of a single specified system id; can add just objects in that system
        int system_id = m_system_id->Eval(parent_context);
        auto system = GetSystem(system_id);
        if (!system)
            return;
    
        const ObjectMap& obj_map = Objects();
        const std::set<int>& system_object_ids = system->ObjectIDs();
        auto sys_objs = obj_map.FindObjects(system_object_ids);
    
        // insert all objects that have the specified system id
        condition_non_targets.reserve(sys_objs.size() + 1);
        std::copy(sys_objs.begin(), sys_objs.end(), std::back_inserter(condition_non_targets));
        // also insert system itself
        condition_non_targets.push_back(system);
    }
    
    bool InSystem::Match(const ScriptingContext& local_context) const {
        auto candidate = local_context.condition_local_candidate;
        if (!candidate) {
            ErrorLogger() << "InSystem::Match passed no candidate object";
            return false;
        }
        int system_id = (m_system_id ? m_system_id->Eval(local_context) : INVALID_OBJECT_ID);
        return InSystemSimpleMatch(system_id)(candidate);
    }
    
    void InSystem::SetTopLevelContent(const std::string& content_name) {
        if (m_system_id)
            m_system_id->SetTopLevelContent(content_name);
    }
    
    unsigned int InSystem::GetCheckSum() const {
        unsigned int retval{0};
    
        CheckSums::CheckSumCombine(retval, "Condition::InSystem");
        CheckSums::CheckSumCombine(retval, m_system_id);
    
        TraceLogger() << "GetCheckSum(InSystem): retval: " << retval;
        return retval;
    }
    
    Now we've located the actual C++ code that the FOCS "InSystem" is translated into, so we can start trying to figure out what's "really" going on. Generally this is the point at which i start to get really confused, but this is more or less the process that i use, and sometimes it's helpful.

    In this case, IF i'm reading it correctly (big if), InSystem returns a boolean equal to whether the object has an m_system_id (i.e. whether the object is located in a system), and InSystem id returns the m_system_id of the system the object is in.
Now i'll try to answer your questions (to the best of my ability):
Oberlus wrote:
Thu Nov 21, 2019 11:21 am
First of all, Source is the ship part SP_KRILL_SPAWNER itself, right?
Yes.
Oberlus wrote:
Thu Nov 21, 2019 11:21 am
So this applies to asteroid belts in the same system than the ship mounting the SP_KRILL_SPAWNER, right?
Yes, to unowned asteroid belts in the same system as the ship with the krill spawner.
Oberlus wrote:
Thu Nov 21, 2019 11:21 am
InSystem? That is not travelling a starlane? But isn't now the Target an unowned asteroid belt that should not be travelling? Or maybe this is asking for the Target to be in the same system than Source? Or just this is asking for a System, in general?
InSystem should just be checking whether the object is in a system. It seems redundant here, as scope has already limited the options to planets in the same system as the krill spawner part, so maybe i'm missing something, or maybe this is just a redundant check to avoid potential errors.
Oberlus wrote:
Thu Nov 21, 2019 11:21 am
an object that is the System in which the SP_KRILL_SPAWNER was?
Oberlus wrote:
Thu Nov 21, 2019 11:21 am
which is itself a system (shouldn't this be implicit from previous line?)
Oberlus wrote:
Thu Nov 21, 2019 11:21 am
that is in the system of the SP_KRILL_SPAWNER?
As far as the 'shouldn't this be implicit' question, in my limited experience, the FOCS code is kludged together by a variety of contributors, so there isn't really much of a coding standard. It's mostly "if it works, it's fine", so there is a fair bit of redundancy. I would tend to agree here, that if we've already checked that the Object''s ID is the same as the Source's SystemID, it shouldn't be necessary to then check whether the Object is a System. Only Systems should have ID's that are SystemID's.

In terms of understanding this section of FOCS code, let's address the next question:
Oberlus wrote:
Thu Nov 21, 2019 11:21 am
So it creates a monster SM_KRILL_1 in... any place that is not the system of the SP_KRILL_SPAWNER as long as it does not have a SM_KRILL_X in there?
What you need to realize is that the set of possible objects where activation is to be considered, has already been limited by the Scope above. We are only checking unowned asteroid belts in the same system as the krill spawner to see if they satisfy the activation condition. Once you've gotten that then you are on the right track, i think.

It creates a monster SM_KRIL_1 in any unowned asteroid belt in the same system as the krill spawner which also satisfies the activation condition. Let me try to break down the activation conditon:

I'll skip the InSystem part, as that seems redundant. That leaves the NotContainedBy And. The And is an intersection, so if we aren't contained by the intersection, that's the same as being not contained by at least one of. In other words NOT (A AND B) = (NOT A) OR (NOT B). If this part is confusing, a Venn diagram can help. Ultimately, if i'm correct, the first two items in this AND (the outer AND) are equivalent in this context.

The point of the statement is to check that the system containing the asteroid belt does not already have a krill ship. If it does then we don't want to generate a new ship. I think it would be fine if the code read:

Code: Select all

activation = Not ContainedBy And [
                    Object id = Source.SystemID //alternatively this could just say System
                    Contains And [
                        Ship
                        //I think we can also eliminate the check to see if the ship is in the system.  Since we are already looking at something contained by the system...
                        Or [
                            DesignHasHull name = "SH_KRILL_1_BODY"
                            DesignHasHull name = "SH_KRILL_2_BODY"
                            DesignHasHull name = "SH_KRILL_3_BODY"
                            DesignHasHull name = "SH_KRILL_4_BODY"
                        ]
                    ]
It would activate on any unowned asteroid belt in the system of the krill spawner part as long as that asteroid belt wasn't contained by a system (i.e. the system that the asteroid belts are in) which contains a ship with a krill body.
________________________________
Well, hopefully i didn't make things worse. I don't really understand this stuff very well, but i tried. Good luck!

User avatar
Oberlus
Cosmic Dragon
Posts: 1796
Joined: Mon Apr 10, 2017 4:25 pm

Re: Excruciating FOCS doubts

#4 Post by Oberlus » Thu Nov 21, 2019 5:04 pm

Thank you very much, alleryn. I'm copying your system from now on.
I think I've diggested properly all this information. I think now I there are way less chances for me to post another doubt.

User avatar
Geoff the Medio
Programming, Design, Admin
Posts: 12499
Joined: Wed Oct 08, 2003 1:33 am
Location: Munich

Re: Excruciating FOCS doubts

#5 Post by Geoff the Medio » Thu Nov 21, 2019 7:18 pm

alleryn wrote:
Thu Nov 21, 2019 4:10 pm
Oberlus wrote:
Thu Nov 21, 2019 11:21 am
First of all, Source is the ship part SP_KRILL_SPAWNER itself, right?
Yes.
Source will be the ship, not the ship part.
Oberlus wrote:
Thu Nov 21, 2019 11:21 am
InSystem? That is not travelling a starlane? But isn't now the Target an unowned asteroid belt that should not be travelling? Or maybe this is asking for the Target to be in the same system than Source? Or just this is asking for a System, in general?
InSystem should just be checking whether the object is in a system. It seems redundant here, as scope has already limited the options to planets in the same system as the krill spawner part, so maybe i'm missing something, or maybe this is just a redundant check to avoid potential errors.
It is scripted like:

Code: Select all

InSystem id = Source.SystemID
The InSystem has a parameter here, id, which specifies which system the object should be in. So this condition will only match objects in the same system as the Source object's system.
What you need to realize is that the set of possible objects where activation is to be considered, has already been limited by the Scope above.
Activation conditions are evaluated on only the source object, and this happens before the scope condition is evaluated.

Activation conditions test the source object (only) to decide whether an effectsgroup should execute. Scope conditions determine what objects to execute on. There may be no overlap between the objects matched by these conditions and having duplicate conditions inside them is not redundant.

I don't know if there is an old general scripting help thread, but the Scripting and Balancing forum would be a good place to post questions: viewforum.php?f=15

Post Reply