# QuArK  -  Quake Army Knife
#
# Copyright (C) 1996-2000 Armin Rigo
# THIS FILE IS PROTECTED BY THE GNU GENERAL PUBLIC LICENCE
# FOUND IN FILE "COPYING.TXT"
#
#$Header: /cvsroot/quark/runtime/plugins/mapbotwaypointer.py,v 1.3 2003/12/18 21:51:46 peter-b Exp $

## Fredrick_vamstad@hotmail.com

Info = {
   "plug-in":       "Bot Waypointer",
   "desc":          "Bot Waypointer",
   "date":          "? may 2002",
   "author":        "Decker",
   "author e-mail": "decker@planetquake.com",
   "quark":         "Version 6.x"
}

import sys
import struct
import quarkx
import quarkpy.qmacro
import quarkpy.mapmenus
from quarkpy.qeditor import *
from quarkpy.mapduplicator import *
from quarkpy.qhandles import *

#
# Base class for load/save bot-waypoint files
#
class BotWaypointConverter:
    m_waypoints = []

    def __init__(self):
        pass

    def Load(self, filename):
        raise

    def Save(self, filename):
        raise

#
# Class to load/save HPBBot v2.1 waypoint files
#
class BotWaypointConvertHPB(BotWaypointConverter):
    "HPB-Bot v2.1 .WPT file load/save"

    m_HPB_flags = \
        [(      1 ,"team_bit1"        ,"allow for 4 teams (0-3)"                                   ) \
        ,(      2 ,"team_bit2"        ,"allow for 4 teams (0-3)"                                   ) \
        ,(      4 ,"teamspecific"     ,"waypoint only for specified team"                          ) \
        ,(      8 ,"crouch"           ,"must crouch to reach this waypoint"                        ) \
        ,(     16 ,"ladder"           ,"waypoint on a ladder"                                      ) \
        ,(     32 ,"lift"             ,"wait for lift to be down before approaching this waypoint" ) \
        ,(     64 ,"door"             ,"wait for door to open"                                     ) \
        ,(    128 ,"health"           ,"health kit (or wall mounted) location"                     ) \
        ,(    256 ,"armor"            ,"armor (or HEV) location"                                   ) \
        ,(    512 ,"ammo"             ,"ammo location"                                             ) \
        ,(   1024 ,"sniper"           ,"sniper waypoint (a good sniper spot)"                      ) \
        ,(   2048 ,"flag"             ,"flag position (or hostage or president)"                   ) \
        ,(   2048 ,"flf_cap"          ,"Front Line Force capture point"                            ) \
        ,(   4096 ,"flag_goal"        ,"flag return position (or rescue zone)"                     ) \
        ,(   4096 ,"flf_defend"       ,"Front Line Force defend point"                             ) \
        ,(   8192 ,"prone"            ,"go prone (laying down)"                                    ) \
        ,(  16384 ,"aiming"           ,"aiming waypoint"                                           ) \
        ,(  32768 ,"sentrygun"        ,"sentry gun waypoint for TFC"                               ) \
        ,(  65536 ,"dispenser"        ,"dispenser waypoint for TFC"                                ) \
        ,( 131072 ,"weapon"           ,"weapon_ entity location"                                   ) \
        ,( 262144 ,"jump"             ,"jump waypoint"                                             ) \
        ]

    def FlagsToSpecs(self, flags, obj):
        for flag, spec, hint in self.m_HPB_flags:
            if (flag & flags) == flag:
                obj["_"+spec] = "1"
            else:
                obj["_"+spec] = "0"

    def SpecsToFlags(self, obj):
        flags = 0
        for flag, spec, hint in self.m_HPB_flags:
            try:
                flags = flags + (flag * int(obj["_"+spec] == "1"))
            except:
                pass
        return flags

    def Load(self, filename):
        self.m_waypoints = []

        f = open(filename, "rb")

        # Info on the 'struct' object: http://www.python.org/doc/current/lib/module-struct.html

        # Read the header
        filetype = f.read(8)
        waypoint_file_version, waypoint_file_flags, number_of_waypoints = struct.unpack("iii", f.read(4*3))
        mapname = f.read(32)

        # Check the header
        if (filetype != "HPB_bot\x00"):
            raise "Fail in file-header: Not 'HPB_bot', but '" + filetype + "'"
        if (waypoint_file_version != 4):
            raise "Fail in file-header: Not version '4', but '" + str(waypoint_file_version) +"'"

        # Read waypoint-coordinates
        for wp in range(number_of_waypoints):
            flags, vecx, vecy, vecz = struct.unpack("ifff", f.read(4 + (4*3)))

            obj = quarkx.newobj("dup botwaypointerpoint:d")
            obj["macro"] = "dup botwaypointerpoint"
            obj["origin"] = str(vecx)+" "+str(vecy)+" "+str(vecz)
            obj.translate(quarkx.vect(0, 0, 0))
            obj["targetname"] = "wp"+str(wp)
            self.FlagsToSpecs(flags, obj)

            self.m_waypoints.append(obj)

        # Read waypoint-paths
        for wp in range(number_of_waypoints):
            obj = self.m_waypoints[wp]
            num, = struct.unpack("H", f.read(2))
            for p in range(num):
                wp_idx, = struct.unpack("H", f.read(2))
                obj["target"+str(p)] = "wp"+str(wp_idx)

        f.close()

    def Save(self, filename):
        f = open(filename, "wb")

        # Write the header
        f.write("HPB_bot\x00")
        f.write(struct.pack("iii", 4, 0, len(self.m_waypoints)))
        data = filename.split("\\")[-1]
        data = data.split(".")[0]
        data = data + "\x00"*(31-len(data))+"\x00"
        f.write(data)

        # Write waypoint-coordinates
        for wp in range(len(self.m_waypoints)):
            obj = self.m_waypoints[wp]

            flags = self.SpecsToFlags(obj)
            f.write(struct.pack("i", flags))

            vecx, vecy, vecz = quarkx.vect(obj["origin"]).tuple
            f.write(struct.pack("fff", vecx, vecy, vecz))

        # Write waypoint-paths
        for wp in range(len(self.m_waypoints)):
            obj = self.m_waypoints[wp]
            p = 0
            wp_idxs = ""
            for i in range(16):
                arg = obj["target"+str(i)]
                if ((arg is not None) and (arg != "")):
                    for o in range(len(self.m_waypoints)):
                        if (self.m_waypoints[o]["targetname"] == arg):
                            wp_idxs = wp_idxs + struct.pack("H", o)
                            p = p + 1
                            break
            f.write(struct.pack("H", p))
            f.write(wp_idxs)

        f.close()

#
# Class to load/save ACEBot/LTKBot waypoint files
#
class BotWaypointConvertACE(BotWaypointConverter):
    "ACEBot .LTK file load/save"

    m_ACE_types = \
        [(0 ,"MOVE"         ,"" ) \
        ,(1 ,"LADDER"       ,"" ) \
        ,(2 ,"PLATFORM"     ,"" ) \
        ,(3 ,"TELEPORTER"   ,"" ) \
        ,(4 ,"ITEM"         ,"" ) \
        ,(5 ,"WATER"        ,"" ) \
        ,(6 ,"GRAPPLE"      ,"" ) \
        ,(7 ,"JUMP"         ,"" ) \
        ,(8 ,"DOOR"         ,"" ) \
        ]

    def TypeToSpec(self, master_type, obj):
        for type, spec, hint in self.m_ACE_types:
            if (type == master_type):
                obj["type"] = spec
                break

    def SpecToType(self, obj):
        for type, spec, hint in self.m_ACE_types:
            if (obj["type"] == spec):
                return type
        raise "ACEBot: Unknown spec-for-type: '"+obj["type"]+"'"

    def Load(self, filename):
        self.m_waypoints = []

        f = open(filename, "rb")

        # Read the header
        version, = struct.unpack("i", f.read(4)) # int

        # Check the header
        if (version != 4):
            raise "Fail in file-header: Not version '4', but '" + str(version) + "'"

        num_of_nodes, = struct.unpack("i", f.read(4))    # int
        num_of_items, = struct.unpack("i", f.read(4))    # int

        # setup a progress-indicator
        progressbar = quarkx.progressbar(509, (num_of_nodes * 2) + num_of_items)
        try:

            # read nodes
            for i in range(num_of_nodes):
                progressbar.progress()
                vecx, vecy, vecz = struct.unpack("fff", f.read(4*3))    # vec3_t
                node_type, = struct.unpack("i", f.read(4))              # int
                node_num, dummy = struct.unpack("hh", f.read(2+2))      # short int (+ 'short int' padded)

                obj = quarkx.newobj("dup botwaypointerpoint:d")
                obj["macro"] = "dup botwaypointerpoint"
                obj["origin"] = str(vecx)+" "+str(vecy)+" "+str(vecz)
                obj.translate(quarkx.vect(0, 0, 0))
                obj["targetname"] = "wp"+str(node_num)
                self.TypeToSpec(node_type, obj)

                for j in range(12):     # 12 = MAXLINKS
                    target_node, dummy, cost = struct.unpack("hhf", f.read(2+2+4)) # short int (+ 'short int' padded) float

                    if (target_node > 0):
                        obj["target"+str(j)] = "wp"+str(target_node)
                        obj["target"+str(j)+"_cost"] = str(int(cost))

                self.m_waypoints.append(obj)

            # read path_table, and compress it into minimal number of specs.
            for i in range(num_of_nodes):
                progressbar.progress()
                obj = self.m_waypoints[i]
                for j in range(num_of_nodes):
                    path_to_node, = struct.unpack("h", f.read(2)) # short int
                    if (path_to_node >= 0):
                        try:
                            value = obj["via_wp"+str(path_to_node)+"_to"] + ";" + "wp"+str(j)
                        except:
                            value = "wp"+str(j)
                        obj["via_wp"+str(path_to_node)+"_to"] = value

            # read items
            for i in range(num_of_items):
                progressbar.progress()
                item, weight, ent_ptr, node = struct.unpack("ifii", f.read(4 + 4 + 4 + 4))

        finally:
            progressbar.close()

        f.close()

    def Save(self, filename):
        f = open(filename, "wb")

        # Write the header
        f.write(struct.pack("i", 4)) # int - version 4

        num_of_nodes = len(self.m_waypoints)
        num_of_items = 0

        data = struct.pack("i", num_of_nodes) + struct.pack("i", num_of_items) # int int
        f.write(data)

        # Build translation-table for 'targetnames_to_nodenum'
        targetnames_to_nodenum = {}
        for i in range(num_of_nodes):
            obj = self.m_waypoints[i]
            targetnames_to_nodenum[obj["targetname"]] = i

        # setup a progress-indicator
        progressbar = quarkx.progressbar(5450, (num_of_nodes * 2) + num_of_items)
        try:

            # Write nodes
            for i in range(num_of_nodes):
                progressbar.progress()
                obj = self.m_waypoints[i]

                try:
                    vecx, vecy, vecz = quarkx.vect(obj["origin"]).tuple
                    f.write(struct.pack("fff", vecx, vecy, vecz))
                except:
                    raise "Error writing origin for "+obj["targetname"]

                try:
                    f.write(struct.pack("i", self.SpecToType(obj)))
                except:
                    raise "Error writing type for "+obj["targetname"]

                try:
                    node_num = targetnames_to_nodenum[obj["targetname"]]
                    dummy = 0
                    f.write(struct.pack("hh", node_num, dummy))
                except:
                    raise "Error writing node_num for "+obj["targetname"]

                cnt = 12
                for j in range(12):
                    target_node = obj["target"+str(j)]
                    cost = obj["target"+str(j)+"_cost"]
                    if ((target_node is not None) and (target_node != "")):
                        try:
                            node_num = targetnames_to_nodenum[target_node]
                            dummy = 0
                            f.write(struct.pack("hhf", node_num, dummy, float(cost))) # short int (+ 'short int' padded) float
                            cnt = cnt - 1
                        except:
                            raise "Error writing nodelist #"+str(j)+" for "+obj["targetname"]
                for j in range(cnt):
                    f.write(struct.pack("hhf", -1, 0, 0))

            # Write path_table. Uncompress it from its minimal 'structure'
            for i in range(num_of_nodes):
                progressbar.progress()
                obj = self.m_waypoints[i]

                node_path = []
                for j in range(num_of_nodes):
                    node_path = node_path + [int(-1)]

                for spec in obj.dictspec.keys():
                    if (spec[:4] == "via_"):
                        wp_num = spec.split("_")[1]
                        via_node_num = targetnames_to_nodenum[wp_num]
                        arg = obj[spec]
                        for wp_num in arg.split(";"):
                            node_path[targetnames_to_nodenum[wp_num]] = via_node_num

                for j in range(num_of_nodes):
                    f.write(struct.pack("h", node_path[j]))

            # Write items
            for i in range(num_of_items):
                progressbar.progress()

        finally:
            progressbar.close()

        f.close()

#
# Supported file-extension types
#
gBotFileExtFilter = ["Supported bot-types|*.wpt;*.ltk", "HPBBot (*.wpt)|*.wpt", "ACEBot/LTKBot (*.ltk)|*.ltk"]

#
# Load macro
#
def macro_botwaypointer_loadfile(self):
    editor = mapeditor()
    if editor is None:
        return
    dup = editor.layout.explorer.uniquesel
    if dup is None:
        return

    files = quarkx.filedialogbox("Load bot waypoint file...", "", gBotFileExtFilter, 0, "*.*")
    if len(files) == 1:
        files[0] = files[0].lower()
        if (files[0][-3:] == "wpt"):
            aObj = BotWaypointConvertHPB()
        elif (files[0][-3:] == "ltk"):
            aObj = BotWaypointConvertACE()
        else:
            raise "File-extension not supported '"+files[0][-3:]+"'."

        aObj.Load(files[0])

        # Setup undo/redo possibility
        undo = quarkx.action()

        undo.setspec(dup, "last_file", files[0])

        # Remove old waypoints (everything below the selected duplicator-object)
        for d in dup.subitems:
            undo.exchange(d, None)

        # Add the new waypoints
        for w in aObj.m_waypoints:
            undo.put(dup, w)

        # Do the undo/redo stuff, and redraw the views.
        editor.ok(undo, 'Load bot waypoint file')

        editor.layout.explorer.sellist = [dup]
        editor.invalidateviews()

quarkpy.qmacro.MACRO_botwaypointer_loadfile = macro_botwaypointer_loadfile

#
# Save macro
#
def macro_botwaypointer_savefile(self):
    editor = mapeditor()
    if editor is None:
        return
    dup = editor.layout.explorer.uniquesel
    if dup is None:
        return

    last_file = dup["last_file"]

    files = quarkx.filedialogbox("Save bot waypoint file...", "", gBotFileExtFilter, 1, last_file)
    if len(files) == 1:
        files[0] = files[0].lower()
        if (files[0][-3:] == "wpt"):
            aObj = BotWaypointConvertHPB()
        elif (files[0][-3:] == "ltk"):
            aObj = BotWaypointConvertACE()
        else:
            raise "File-extension not supported '"+files[0][-3:]+"'."

        dup["last_file"] = files[0]

        # Store the waypoints
        for w in dup.subitems:
            aObj.m_waypoints.append(w)

        aObj.Save(files[0])

quarkpy.qmacro.MACRO_botwaypointer_savefile = macro_botwaypointer_savefile


#
#
#
colors = [0xB00000,
          0x00B000,
          0x0000B0,
          0xB0B000,
          0x00B0B0,
          0xB000B0,
          0xB0B0B0]

def FindEntityByTargetname(name, list):
    for e in list:
        if (e["targetname"] == name):
            return e
    return None

def ShortestRouteTree(view, cv, obj, waypoints, cnt=0):
    if (cnt >= 90):
        # a simple test to ensure we don't get into an endless loop
        print "Possible cyclic path:", cnt, obj["targetname"], waypoints
        if (cnt >= 100):
            # stop traversing
            return 1

    pp2 = view.proj(obj.origin)
    for spec in obj.dictspec.keys():
        if (spec[0:4] == "via_"):
            viaobj = FindEntityByTargetname(spec.split("_")[1], obj.treeparent.subitems)
            if (viaobj is not None):
                routes = obj[spec].split(";")

                # Use only those waypoint-names, which exists in both lists ('routes' and 'waypoints').
                def isinlist(wp, routes=routes):
                    if wp in routes:
                        return 1
                    return 0
                theseroutes = filter(isinlist, waypoints)

                # Remove from the 'waypoints' list, those waypoint-names who also exists in the 'theseroutes' list.
                def isnotinlist(wp, theseroutes=theseroutes):
                    if wp in theseroutes:
                        return 0
                    return 1
                newwaypoints = filter(isnotinlist, waypoints)
                waypoints = newwaypoints

                # If there are any waypoint-names in 'theseroutes' list, traverse them too.
                if (len(theseroutes) > 0):
                    cv.line(view.proj(viaobj.origin), pp2)
                    if (ShortestRouteTree(view, cv, viaobj, theseroutes, cnt+1) != 0):
                        return 1
    return 0

#
#
#
def FindSelectedOne(list):
    for e in list:
        if (e.selected):
            return e
    return None

#
#
#
class BotWaypointerPointHandle(CenterHandle):

    def __init__(self, points_to_list, routes_to_list, pos, centerof, color=RED, caninvert=0):
        CenterHandle.__init__(self, pos, centerof, color, caninvert)
        self.points_to_list = points_to_list
        self.routes_to_list = routes_to_list
        self.hint = "'"+centerof["targetname"]+"' = targetname.||The black thin arrows, shows which other waypoints this one directly points to, by using its 'target##' specifics.\n\nThe thick colored lines illustrates its nearest neighbours, which are used for the shortest-path-matrix, by using its 'via_wp###_to' specifics."

    def draw(self, view, cv, draghandle=None):
        myparent = self.centerof.treeparent
        myself = self.centerof
        if (myself.selected):
            myparent["lastwaypointorigin"] = myself["origin"]
        if (myparent["shortestpathdisplay"] == "1"):
            if (myself.selected):
                nextclr = 0
                for spec in myself.dictspec.keys():
                    if (spec[0:4] == "via_"):
                        cv.pencolor = colors[nextclr]
                        nextclr = (nextclr + 1) % 6
                        viaobj = FindEntityByTargetname(spec.split("_")[1], myparent.subitems)
                        if (viaobj is not None):
                            pp2 = view.proj(viaobj.origin)
                            cv.penwidth = 3
                            cv.line(view.proj(self.pos), pp2)
                            cv.penwidth = 1
                            via_routes = myself[spec].split(";")
                            if (ShortestRouteTree(view, cv, viaobj, via_routes) != 0):
                                raise "Possible cyclic path"
                           #for wp in via_routes:
                           #    obj = FindEntityByTargetname(wp, myparent.subitems)
                           #    if (obj is not None):
                           #        cv.line(view.proj(obj.origin), pp2)
        else:
            if (self.routes_to_list is not None):
                cv.penwidth = 3
                nextclr = 0
                pp2 = view.proj(self.pos)
                for route_to in self.routes_to_list:
                    cv.pencolor = colors[nextclr]
                    nextclr = (nextclr + 1) % 6
                    cv.line(view.proj(route_to), pp2)
        if (self.points_to_list is not None):
            cv.pencolor = 0
            cv.penwidth = 1
            for point_to in self.points_to_list:
                Arrow(cv, view, self.pos, point_to)
        CenterHandle.draw(self, view, cv, draghandle)


    def menu(self, editor, view):

        def AddTwoWayClick(m, self=self, editor=editor):
            myparent = self.centerof.treeparent
            myself   = self.centerof
            theSelected = FindSelectedOne(myparent.subitems)
            if (theSelected is not None):
                undo = quarkx.action()

                try:
                    use_specname = None
                    use_num = 0
                    for spec in theSelected.dictspec.keys():
                        if (spec[:6] == "target" and spec != "targetname"):
                            if (theSelected[spec] == myself["targetname"]):
                                raise "already exist"
                            if (theSelected[spec] is None or theSelected[spec] == ""):
                                use_specname = spec
                            use_num = max(use_num, int(spec[6:]))
                    if (use_specname is None):
                        use_specname = "target" + str(use_num + 1)
                    undo.setspec(theSelected, use_specname, myself["targetname"])
                except:
                    pass

                try:
                    use_specname = None
                    use_num = 0
                    for spec in myself.dictspec.keys():
                        if (spec[:6] == "target" and spec != "targetname"):
                            if (myself[spec] == theSelected["targetname"]):
                                raise "already exist"
                            if (myself[spec] is None or myself[spec] == ""):
                                use_specname = spec
                            use_num = max(use_num, int(spec[6:]))
                    if (use_specname is None):
                        use_specname = "target" + str(use_num + 1)
                    undo.setspec(myself, use_specname, theSelected["targetname"])
                except:
                    pass

                undo.ok(editor.Root, "add two-way target")
                editor.invalidateviews()

        def AddOneWayClick(m, self=self, editor=editor):
            myparent = self.centerof.treeparent
            myself   = self.centerof
            theSelected = FindSelectedOne(myparent.subitems)
            if (theSelected is not None):
                undo = quarkx.action()

                try:
                    use_specname = None
                    use_num = 0
                    for spec in theSelected.dictspec.keys():
                        if (spec[:6] == "target" and spec != "targetname"):
                            if (theSelected[spec] == myself["targetname"]):
                                raise "already exist"
                            if (theSelected[spec] is None or theSelected[spec] == ""):
                                use_specname = spec
                            use_num = max(use_num, int(spec[6:]))
                    if (use_specname is None):
                        use_specname = "target" + str(use_num + 1)
                    undo.setspec(theSelected, use_specname, myself["targetname"])
                except:
                    pass

                undo.ok(editor.Root, "add two-way target")
                editor.invalidateviews()

        def RemTwoWayClick(m, self=self, editor=editor):
            myparent = self.centerof.treeparent
            myself   = self.centerof
            theSelected = FindSelectedOne(myparent.subitems)
            if (theSelected is not None):
                undo = quarkx.action()
                for spec in theSelected.dictspec.keys():
                    if (theSelected[spec] == myself["targetname"]):
                        undo.setspec(theSelected, spec, None)
                for spec in myself.dictspec.keys():
                    if (myself[spec] == theSelected["targetname"]):
                        undo.setspec(myself, spec, None)
                undo.ok(editor.Root, "remove two-way target")
                editor.invalidateviews()

        def RemOneWayClick(m, self=self, editor=editor):
            myparent = self.centerof.treeparent
            myself   = self.centerof
            theSelected = FindSelectedOne(myparent.subitems)
            if (theSelected is not None):
                undo = quarkx.action()
                for spec in theSelected.dictspec.keys():
                    if (theSelected[spec] == myself["targetname"]):
                        undo.setspec(theSelected, spec, None)
                undo.ok(editor.Root, "remove one-way target")
                editor.invalidateviews()

        def ShortestPathDisplayClick(m, self=self, editor=editor):
            myparent = self.centerof.treeparent
            shortestpathdisplay = not int(myparent["shortestpathdisplay"])
            m.state = quarkpy.qmenu.checked and shortestpathdisplay
            myparent["shortestpathdisplay"] = str(shortestpathdisplay)
            editor.invalidateviews()

        myparent = self.centerof.treeparent

        menu_add_twoway = quarkpy.qmenu.item("Add two-way target",    AddTwoWayClick, "|stuff to type here...")
        menu_add_oneway = quarkpy.qmenu.item("Add one-way target",    AddOneWayClick, "|stuff to type here...")
        menu_rem_twoway = quarkpy.qmenu.item("Remove two-way target", RemTwoWayClick, "|stuff to type here...")
        menu_rem_oneway = quarkpy.qmenu.item("Remove one-way target", RemOneWayClick, "|stuff to type here...")

        theSelected = FindSelectedOne(myparent.subitems)

        # Enable/disable menu-items depending on the state and spec/args of the two in question.
        if (theSelected is None or theSelected == self.centerof):
            menu_add_twoway.state = quarkpy.qmenu.disabled
            menu_add_oneway.state = quarkpy.qmenu.disabled
            menu_rem_twoway.state = quarkpy.qmenu.disabled
            menu_rem_oneway.state = quarkpy.qmenu.disabled
        else:
            # Figure out, who targets who.
            myself_targetname       = self.centerof["targetname"]
            theselected_targetname  = theSelected["targetname"]
            i_target_selected   = 0
            selected_targets_me = 0
            for spec in self.centerof.dictspec.keys():
                if (spec[:6] == "target" and self.centerof[spec] == theselected_targetname):
                    i_target_selected = 1
                    break
            for spec in theSelected.dictspec.keys():
                if (spec[:6] == "target" and theSelected[spec] == myself_targetname):
                    selected_targets_me = 1
                    break
            menu_add_twoway.state = not (not i_target_selected or not selected_targets_me) and quarkpy.qmenu.disabled
            menu_add_oneway.state = not (not selected_targets_me)                          and quarkpy.qmenu.disabled
            menu_rem_twoway.state = not (i_target_selected and selected_targets_me)        and quarkpy.qmenu.disabled
            menu_rem_oneway.state = not (selected_targets_me)                              and quarkpy.qmenu.disabled

        # Set up 'shortest path display' menuitem-checkbox
        menu_shortestpath = quarkpy.qmenu.item("Shortest-path display", ShortestPathDisplayClick, "|stuff to type here...")
        try:
            shortestpathdisplay = int(myparent["shortestpathdisplay"])
        except:
            shortestpathdisplay = 0
        myparent["shortestpathdisplay"] = str(shortestpathdisplay)
        menu_shortestpath.state = quarkpy.qmenu.checked and shortestpathdisplay

        return [menu_add_twoway, menu_add_oneway, menu_rem_twoway, menu_rem_oneway, qmenu.sep, menu_shortestpath]

#
#
#
class BotWaypointerPoint(DuplicatorManager):

    def buildimages(self, singleimage=None):
        pass

    def handles(self, editor, view):
        myparent = self.dup.treeparent
        myself = self.dup
        hndls = []

        # Examine the spec/args for directly associated waypoints to myself
        points_to_list = []
        routes_to_list = []
        to_list = []
        for spec in myself.dictspec.keys():
            if (spec[:6] == "target" and myself[spec] is not None and myself[spec] != ""):
                points_to = FindEntityByTargetname(myself[spec], myparent.subitems)
                if (points_to is not None):
                    points_to_list.append(points_to.origin)
                    if (points_to not in to_list):
                        to_list.append(points_to)
            if (spec[:4] == "via_" and myself[spec] is not None and myself[spec] != ""):
                routes_to = FindEntityByTargetname(spec.split("_")[1], myparent.subitems)
                if (routes_to is not None):
                    routes_to_list.append(routes_to.origin)
                    if (routes_to not in to_list):
                        to_list.append(points_to)

        # First add the selected handle, so it will be drawn first (z-order of drawn lines)
        hndls.append(BotWaypointerPointHandle(points_to_list, routes_to_list, self.dup.origin, self.dup))

        # Then append those found in the spec/args.
        def createhandle(o):
            return BotWaypointerPointHandle(None, None, o.origin, o)
        hndls = hndls + map(createhandle, to_list)

        # Lastly show also the rest within a sphere of 512 units
        for obj in myparent.subitems:
            if (obj in hndls):
                # Don't take objects with twice!
                continue
            if abs(obj.origin - self.dup.origin) < 512:
                # Only those within 512 units
                hndls.append(BotWaypointerPointHandle(None, None, obj.origin, obj))

        # And return the list of handles
        return hndls


#
#
#
class BotWaypointer(StandardDuplicator):

    def buildimages(self, singleimage=None):
        # Don't allow dissociate images to work
        if (singleimage is not None):
            return []
        # Local function to create dummy entity, so the waypoints become visible in the 2D-views
        # without the need to have this duplicator selected.
        def makeentitiy(obj):
            newobj = quarkx.newobj("info_waypoint:e")
            newobj["origin"] = str(obj.origin)
            newobj.translate(quarkx.vect(0, 0, 0)) # damn - if this isn't here, the entities will not show in the views
            return newobj
        # Create the list of dummy entities
        botwaypointerpointentities = map(makeentitiy, self.dup.subitems)
        return botwaypointerpointentities

    def handles(self, editor, view):
        def makehandle(obj):
            return quarkpy.qhandles.CenterHandle(obj.origin, obj)
        botwaypointerpointhandles = map(makehandle, self.dup.subitems)
        return botwaypointerpointhandles

#
#
#
def NewWaypointTargetname(list):
    MaxNum = 0
    for e in list:
        if (e["targetname"][:2] == "wp"):
            Num = int(e["targetname"][2:])
            if (Num > MaxNum):
                MaxNum = Num
    MaxNum = MaxNum + 1
    MaxTargetname = "wp" + str(MaxNum)

    return MaxTargetname

#
#
#
def PasteBotWaypointClick(m):
    editor = mapeditor()
    if (editor is None):
        return
    # Check that the correct object is in the ClipBoard
    clipboard = quarkx.pasteobj(1)
    if ((len(clipboard) != 1) or (clipboard[0]["macro"] != "dup botwaypointerpoint")):
        quarkx.msgbox("Clipboard does not contain exactly one 'dup botwaypointerpoint' entity.", MT_ERROR, MB_OK)
        return
    # Find our master 'Bot Waypointer' (must be only one named like that)
    masters = editor.Root.findallsubitems("Bot Waypointer", ':d')
    if (len(masters) != 1):
        if (len(masters) > 1):
            quarkx.msgbox("A single 'Bot Waypointer' entity could not be determined. Please remove or rename the ones you are not editing for to something else.", MT_ERROR, MB_OK)
        else:
            quarkx.msgbox("Could not find a 'Bot Waypointer' entity. May it have been renamed to something else?", MT_ERROR, MB_OK)
        return
    master = masters[0]

    #
    newobj = clipboard[0].copy()
    x, y, z = m.pos.x, m.pos.y, m.pos.z
    mx, my, mz = tuple(master["lastwaypointorigin"].split())
    if   m.view.info["type"] == "XY": z = float(mz)
    elif m.view.info["type"] == "XZ": y = float(my)
    elif m.view.info["type"] == "YZ": x = float(mx)
    newobj["origin"] = str(editor.aligntogrid(quarkx.vect(x, y, z)))
    newobj.translate(quarkx.vect(0, 0, 0)) # damn - if this isn't here, the entity will not show in the views
    newobj["targetname"] = NewWaypointTargetname(master.subitems)

    undo = quarkx.action()
    undo.put(master, newobj, None)
    undo.setspec(master, "lastwaypointorigin", newobj["origin"])
    undo.ok(editor.Root, "paste waypoint")
    editor.layout.actionmpp()

#
#
#
def backmenu(editor, view=None, origin=None, oldbackmenu=quarkpy.mapmenus.BackgroundMenu):
    # Build the normal context-menu
    menu = oldbackmenu(editor, view, origin)
    # Create a new menu-item, and store the cursor's origin in it.
    menupastebotwaypoint = quarkpy.qmenu.item("&Paste waypoint", PasteBotWaypointClick, "|To paste a bot waypoint, you must first have copied (CTRL+C) a single 'dup botwaypointerpoint' entity into the clipboard.\n\nThis action also creates a new targetname for the waypoint, and keeps it aligned with the previous waypoint selected.")
    menupastebotwaypoint.pos = origin
    menupastebotwaypoint.view = view
    # If there is nothing to paste, disable the menu (note: it could be anything in the clipboard,
    # but we'll test agains that once the user actully performs the action)
    if ((quarkx.pasteobj(0) == 0) or (view is None) or (origin is None)):
        menupastebotwaypoint.state = quarkpy.qmenu.disabled
    # Create a sub-menu that contains the Bot waypointer special menuitems
    popup = quarkpy.qmenu.popup("&Bot waypointer", [menupastebotwaypoint])
    # Put the new sub-menu on top of the context-menu list, and return the lot...
    menu[:0] = [popup]
    return menu

quarkpy.mapmenus.BackgroundMenu = backmenu

#
#
#
quarkpy.mapduplicator.DupCodes.update({
  "dup botwaypointer":       BotWaypointer,
  "dup botwaypointerpoint":  BotWaypointerPoint,
})

# ----------- REVISION HISTORY ------------
# $Log: mapbotwaypointer.py,v $
# Revision 1.3  2003/12/18 21:51:46  peter-b
# Removed reliance on external string library from Python scripts (second try ;-)
#
# Revision 1.2  2002/08/02 19:17:08  decker_dk
# Uhm... Lots of things changed, of which I've forgotten.
#
# Revision 1.1  2002/06/11 17:12:57  decker_dk
# A Bot waypoint-editor. At the moment only works for HPBBot(Half-Life) and LTK/ACEBot(Quake-2)
#
