<commit_before>import math
import os
import sys

from direct.task import Task  # This must be imported first.
from direct.actor.Actor import Actor
from direct.showbase.ShowBase import ShowBase
from panda3d import core
from panda3d.core import Point3, Mat4, Filename, NodePath

from src.shared import config
from src.shared import messages
from src.shared.geometry import chunkToUnit
from src.shared.ident import unitToPlayer
from src.shared.logconfig import newLogger
from src.shared.message_infrastructure import deserializeMessage, \
    illFormedMessage, unhandledMessageCommand, invalidMessageArgument, \
    InvalidMessageError
from src.shared.unit_set import UnitSet
from src.shared.utils import minmax, thisShouldNeverHappen
from src.client.backend import unitToGraphics, GRAPHICS_SCALE

log = newLogger(__name__)


# TODO[#34]: Read from a config file.
DESIRED_FPS = 60


class Entity(object):
    """
    Class to represent any sort of thing with a graphical presence in the
    world: ground, trees, units, structures.
    """

    def __init__(self, graphicId, model, isActor):
        self.gid     = graphicId
        self.model   = model
        self.isActor = isActor

    def cleanup(self):
        if self.isActor:
            self.model.cleanup()
        self.model.removeNode()


class WartsApp(ShowBase):
    """
    The application running all the graphics.
    """

    def __init__(self, graphicsInterface):
        ShowBase.__init__(self)

        self.graphicsInterface = graphicsInterface

        # Mapping from gids to entities.
        self.entities = {}

        # Set up event handling.
        self.keys = {}
        self.setupEventHandlers()
        self.setupMouseHandler()

        # Set up camera control.
        self.cameraHolder = self.render.attachNewNode('CameraHolder')
        self.cameraHolder.setPos(0, 0, 100)
        self.prevCameraHpr = (0, -80, 0)
        self.setCameraCustom()

        self.graphicsInterface.graphicsReady(self)

    def cleanup(self):
        pass

    def interfaceMessage(self, data):
        # Messages from GraphicsInterface to Graphics are always internal
        # client messages, so no need to catch InvalidMessageError.
        message = deserializeMessage(data)
        if isinstance(message, messages.AddEntity):
            self.addEntity(message.gid, message.pos, message.modelPath,
                           message.isExample, message.goalSize)
        elif isinstance(message, messages.RemoveEntity):
            self.removeEntity(message.gid)
        elif isinstance(message, messages.MoveEntity):
            self.moveEntity(message.gid, message.pos)
        else:
            unhandledInternalMessage(message, log)

    def addEntity(self, gid, pos, modelPath, isExample, goalSize):
        """
        pos is given in graphics coordinates.

        goalSize, if specified, is a pair (width, height) -- the model will be
        scaled in the xy plane so that it's as large as possible while still
        fitting within that width and height. Don't pass 0 as the width or the
        height, because that's just not nice.
        """

        if gid in self.entities:
            raise RuntimeError("Already have entity with gid {gid}."
                               .format(gid=gid))

        log.debug("Adding graphical entity {} at {}".format(gid, pos))
        x, y = pos

        if isExample:
            # The example panda from the Panda3D "Hello world" tutorial.
            # TODO[#9]: Figure out a more general way of specifying animations.
            model = Actor(modelPath,
                          {"walk": "models/panda-walk4"})
        else:
            model = self.loader.loadModel(getModelPath(modelPath))
        # Put the model in the scene, but don't position it yet.
        model.reparentTo(self.render)

        # Rescale the model about its origin. The x and y coordinates of the
        # model's origin should be chosen as wherever it looks like the model's
        # center of mass is, so that rotation about the origin (in the xy
        # plane) feels natural.

        goalWidthX, goalWidthY = goalSize

        bound1, bound2 = model.getTightBounds()
        modelWidthX = abs(bound2[0] - bound1[0])
        modelWidthY = abs(bound2[1] - bound1[1])

        xScale = goalWidthX / modelWidthX
        yScale = goalWidthY / modelWidthY

        # Scale it to the largest it can be while still fitting within the goal
        # rect. If the aspect ratio of the goal rect is different from that of
        # the model, then it'll only fill that rect in one dimension.
        # altScaleFactor is used for sanity checks below.
        scaleFactor, altScaleFactor = minmax(goalWidthX / modelWidthX,
                                             goalWidthY / modelWidthY)

        # Sanity check the scale factor.
        if scaleFactor <= 0.0:
            if scaleFactor == 0.0:
                log.warn("Graphical entity {} will be scaled negatively!"
                         .format(gid))
            else:
                log.warn("Graphical entity {} will be scaled to zero size."
                         .format(gid))
        else:
            # TODO[#9]: Currently the example panda triggers this warning.
            # TODO[#3]: Magic numbers bad.
            if altScaleFactor / scaleFactor > 1.001:
                log.warn("Graphical entity {} has different aspect ratio "
                         "than its model: model of size {:.3g} x {:.3g} "
                         "being scaled into {:.3g} x {:.3g}."
                         .format(gid, modelWidthX, modelWidthY,
                                 goalWidthX, goalWidthY))

        model.setScale(scaleFactor)

        # Place the model at z=0. The model's origin should be placed so that
        # this looks natural -- for most units this means it should be right at
        # the bottom of the model, but if we add any units that are intended to
        # float above the ground, then this can be accomplished by just
        # positioning the model above its origin.
        model.setPos(x, y, 0.0)

        entity = Entity(gid, model, isExample)
        self.entities[gid] = entity

    def removeEntity(self, gid):
        log.debug("Removing graphical entity {}".format(gid))
        entity = self.entities.pop(gid)
        entity.cleanup()

    def moveEntity(self, gid, newPos):
        log.debug("Moving graphical entity {} to {}".format(gid, newPos))
        entity = self.entities[gid]

        x, y = newPos
        oldX, oldY, oldZ = entity.model.getPos()
        z = oldZ

        # Ensure the entity is facing the right direction.
        heading = math.atan2(y - oldY, x - oldX)
        heading *= 180.0 / math.pi
        # Magic angle adjustment needed to stop the panda always facing
        # sideways.
        # TODO[#9]: Establish a convention about which way _our_ models face;
        # figure out whether we need something like this. (Hopefully not?)
        heading += 90.0
        entity.model.setHpr(heading, 0, 0)

        moveInterval = entity.model.posInterval(config.TICK_LENGTH, (x, y, z))
        moveInterval.start()

        if entity.isActor and "walk" in entity.model.getAnimNames():
            currFrame = entity.model.getCurrentFrame("walk")
            if currFrame is None:
                currFrame = 0
            # Supposedly, it's possible to pass a startFrame and a duration to
            # actorInterval, instead of calculating the endFrame ourself. But
            # for some reason, that doesn't seem to work; if I do that, then
            # the animation just keeps jumping around the early frames and
            # never gets past frame 5 or so. I'm not sure why. For now at
            # least, just calculate the endFrame ourselves to work around this.
            log.debug("Animating entity {} from frame {}/{}"
                      .format(gid, currFrame,
                              entity.model.getNumFrames("walk")))
            frameRate = entity.model.getAnimControl("walk").getFrameRate()
            endFrame = currFrame + int(math.ceil(frameRate *
                                                 config.TICK_LENGTH))
            animInterval = entity.model.actorInterval("walk", loop=1,
                startFrame=currFrame, endFrame=endFrame)
            animInterval.start()
<commit_msg>Add nodes between the render and models.<commit_after>import math
import os
import sys

from direct.task import Task  # This must be imported first.
from direct.actor.Actor import Actor
from direct.showbase.ShowBase import ShowBase
from panda3d import core
from panda3d.core import Point3, Mat4, Filename, NodePath

from src.shared import config
from src.shared import messages
from src.shared.geometry import chunkToUnit
from src.shared.ident import unitToPlayer
from src.shared.logconfig import newLogger
from src.shared.message_infrastructure import deserializeMessage, \
    illFormedMessage, unhandledMessageCommand, invalidMessageArgument, \
    InvalidMessageError
from src.shared.unit_set import UnitSet
from src.shared.utils import minmax, thisShouldNeverHappen
from src.client.backend import unitToGraphics, GRAPHICS_SCALE

log = newLogger(__name__)


# TODO[#34]: Read from a config file.
DESIRED_FPS = 60


class Entity(object):
    """
    Class to represent any sort of thing with a graphical presence in the
    world: ground, trees, units, structures.
    """

    def __init__(self, graphicId, model, rootNode, isActor):
        self.gid      = graphicId
        self.model    = model
        self.rootNode = rootNode
        self.isActor  = isActor

    def cleanup(self):
        if self.isActor:
            self.model.cleanup()
        self.model.removeNode()
        self.rootNode.removeNode()


class WartsApp(ShowBase):
    """
    The application running all the graphics.
    """

    def __init__(self, graphicsInterface):
        ShowBase.__init__(self)

        self.graphicsInterface = graphicsInterface

        # Mapping from gids to entities.
        self.entities = {}

        # Set up event handling.
        self.keys = {}
        self.setupEventHandlers()
        self.setupMouseHandler()

        # Set up camera control.
        self.cameraHolder = self.render.attachNewNode('CameraHolder')
        self.cameraHolder.setPos(0, 0, 100)
        self.prevCameraHpr = (0, -80, 0)
        self.setCameraCustom()

        self.graphicsInterface.graphicsReady(self)

    def cleanup(self):
        pass

    def interfaceMessage(self, data):
        # Messages from GraphicsInterface to Graphics are always internal
        # client messages, so no need to catch InvalidMessageError.
        message = deserializeMessage(data)
        if isinstance(message, messages.AddEntity):
            self.addEntity(message.gid, message.pos, message.modelPath,
                           message.isExample, message.goalSize)
        elif isinstance(message, messages.RemoveEntity):
            self.removeEntity(message.gid)
        elif isinstance(message, messages.MoveEntity):
            self.moveEntity(message.gid, message.pos)
        else:
            unhandledInternalMessage(message, log)

    def addEntity(self, gid, pos, modelPath, isExample, goalSize):
        """
        pos is given in graphics coordinates.

        goalSize, if specified, is a pair (width, height) -- the model will be
        scaled in the xy plane so that it's as large as possible while still
        fitting within that width and height. Don't pass 0 as the width or the
        height, because that's just not nice.
        """

        if gid in self.entities:
            raise RuntimeError("Already have entity with gid {gid}."
                               .format(gid=gid))

        log.debug("Adding graphical entity {} at {}".format(gid, pos))
        x, y = pos

        if isExample:
            # The example panda from the Panda3D "Hello world" tutorial.
            # TODO[#9]: Figure out a more general way of specifying animations.
            model = Actor(modelPath,
                          {"walk": "models/panda-walk4"})
        else:
            model = self.loader.loadModel(getModelPath(modelPath))

        # Put the model in the scene, but don't position it yet.
        rootNode = render.attachNewNode("")
        model.reparentTo(rootNode)

        # Rescale the model about its origin. The x and y coordinates of the
        # model's origin should be chosen as wherever it looks like the model's
        # center of mass is, so that rotation about the origin (in the xy
        # plane) feels natural.

        goalWidthX, goalWidthY = goalSize

        bound1, bound2 = model.getTightBounds()
        modelWidthX = abs(bound2[0] - bound1[0])
        modelWidthY = abs(bound2[1] - bound1[1])

        xScale = goalWidthX / modelWidthX
        yScale = goalWidthY / modelWidthY

        # Scale it to the largest it can be while still fitting within the goal
        # rect. If the aspect ratio of the goal rect is different from that of
        # the model, then it'll only fill that rect in one dimension.
        # altScaleFactor is used for sanity checks below.
        scaleFactor, altScaleFactor = minmax(goalWidthX / modelWidthX,
                                             goalWidthY / modelWidthY)

        # Sanity check the scale factor.
        if scaleFactor <= 0.0:
            if scaleFactor == 0.0:
                log.warn("Graphical entity {} will be scaled negatively!"
                         .format(gid))
            else:
                log.warn("Graphical entity {} will be scaled to zero size."
                         .format(gid))
        else:
            # TODO[#9]: Currently the example panda triggers this warning.
            # TODO[#3]: Magic numbers bad.
            if altScaleFactor / scaleFactor > 1.001:
                log.warn("Graphical entity {} has different aspect ratio "
                         "than its model: model of size {:.3g} x {:.3g} "
                         "being scaled into {:.3g} x {:.3g}."
                         .format(gid, modelWidthX, modelWidthY,
                                 goalWidthX, goalWidthY))

        model.setScale(scaleFactor)

        # Place the model at z=0. The model's origin should be placed so that
        # this looks natural -- for most units this means it should be right at
        # the bottom of the model, but if we add any units that are intended to
        # float above the ground, then this can be accomplished by just
        # positioning the model above its origin.
        rootNode.setPos(x, y, 0.0)

        entity = Entity(gid, model, rootNode, isExample)
        self.entities[gid] = entity

    def removeEntity(self, gid):
        log.debug("Removing graphical entity {}".format(gid))
        entity = self.entities.pop(gid)
        entity.cleanup()

    def moveEntity(self, gid, newPos):
        log.debug("Moving graphical entity {} to {}".format(gid, newPos))
        entity = self.entities[gid]

        x, y = newPos
        oldX, oldY, oldZ = entity.rootNode.getPos()
        z = oldZ

        # Ensure the entity is facing the right direction.
        heading = math.atan2(y - oldY, x - oldX)
        heading *= 180.0 / math.pi
        # Magic angle adjustment needed to stop the panda always facing
        # sideways.
        # TODO[#9]: Establish a convention about which way _our_ models face;
        # figure out whether we need something like this. (Hopefully not?)
        heading += 90.0
        entity.rootNode.setHpr(heading, 0, 0)

        moveInterval = entity.rootNode.posInterval(config.TICK_LENGTH,
                                                   (x, y, z))
        moveInterval.start()

        if entity.isActor and "walk" in entity.model.getAnimNames():
            currFrame = entity.model.getCurrentFrame("walk")
            if currFrame is None:
                currFrame = 0
            # Supposedly, it's possible to pass a startFrame and a duration to
            # actorInterval, instead of calculating the endFrame ourself. But
            # for some reason, that doesn't seem to work; if I do that, then
            # the animation just keeps jumping around the early frames and
            # never gets past frame 5 or so. I'm not sure why. For now at
            # least, just calculate the endFrame ourselves to work around this.
            log.debug("Animating entity {} from frame {}/{}"
                      .format(gid, currFrame,
                              entity.model.getNumFrames("walk")))
            frameRate = entity.model.getAnimControl("walk").getFrameRate()
            endFrame = currFrame + int(math.ceil(frameRate *
                                                 config.TICK_LENGTH))
            animInterval = entity.model.actorInterval("walk", loop=1,
                startFrame=currFrame, endFrame=endFrame)
            animInterval.start()

{'fraction_non_alphanumeric': 0.0806098930323267, 'fraction_numerical': 0.007328172093847882, 'mean_word_length': 2.1055239493485045, 'pattern_counts': {'":': 2, '<': 5, '<?xml version=': 0, '>': 5, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 12, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '200481', 'n_tokens_mistral': 4580, 'n_tokens_neox': 4332, 'n_words': 1547}