Browse Source

Merge branch 'master' into webgl-port

rdb 4 years ago
parent
commit
39e033af76

+ 85 - 23
direct/src/fsm/FSM.py

@@ -13,6 +13,8 @@ from direct.showbase.MessengerGlobal import messenger
 from direct.showbase import PythonUtil
 from direct.directnotify import DirectNotifyGlobal
 from direct.stdpy.threading import RLock
+from panda3d.core import AsyncTaskManager, AsyncFuture, PythonTask
+import types
 
 
 class FSMException(Exception):
@@ -27,6 +29,19 @@ class RequestDenied(FSMException):
     pass
 
 
+class Transition(tuple):
+    """Used for the return value of fsm.request().  Behaves like a tuple, for
+    historical reasons."""
+
+    _future = None
+
+    def __await__(self):
+        if self._future:
+            yield self._future
+
+        return tuple(self)
+
+
 class FSM(DirectObject):
     """
     A Finite State Machine.  This is intended to be the base class
@@ -154,6 +169,9 @@ class FSM(DirectObject):
     # must be approved by some filter function.
     defaultTransitions = None
 
+    __doneFuture = AsyncFuture()
+    __doneFuture.set_result(None)
+
     # An enum class for special states like the DEFAULT or ANY state,
     # that should be treatened by the FSM in a special way
     class EnumStates():
@@ -247,7 +265,13 @@ class FSM(DirectObject):
     def forceTransition(self, request, *args):
         """Changes unconditionally to the indicated state.  This
         bypasses the filterState() function, and just calls
-        exitState() followed by enterState()."""
+        exitState() followed by enterState().
+
+        If the FSM is currently undergoing a transition, this will
+        queue up the new transition.
+
+        Returns a future, which can be used to await the transition.
+        """
 
         self.fsmLock.acquire()
         try:
@@ -257,11 +281,13 @@ class FSM(DirectObject):
 
             if not self.state:
                 # Queue up the request.
-                self.__requestQueue.append(PythonUtil.Functor(
-                    self.forceTransition, request, *args))
-                return
+                fut = AsyncFuture()
+                self.__requestQueue.append((PythonUtil.Functor(
+                    self.forceTransition, request, *args), fut))
+                return fut
 
-            self.__setState(request, *args)
+            result = self.__setState(request, *args)
+            return result._future or self.__doneFuture
         finally:
             self.fsmLock.release()
 
@@ -275,6 +301,10 @@ class FSM(DirectObject):
         request is queued up and will be executed when the current
         transition finishes.  Multiple requests will queue up in
         sequence.
+
+        The return value of this function can be used in an `await`
+        expression to suspend the current coroutine until the
+        transition is done.
         """
 
         self.fsmLock.acquire()
@@ -284,12 +314,15 @@ class FSM(DirectObject):
                 self._name, request, str(args)[1:]))
             if not self.state:
                 # Queue up the request.
-                self.__requestQueue.append(PythonUtil.Functor(
-                    self.demand, request, *args))
-                return
+                fut = AsyncFuture()
+                self.__requestQueue.append((PythonUtil.Functor(
+                    self.demand, request, *args), fut))
+                return fut
 
-            if not self.request(request, *args):
+            result = self.request(request, *args)
+            if not result:
                 raise RequestDenied("%s (from state: %s)" % (request, self.state))
+            return result._future or self.__doneFuture
         finally:
             self.fsmLock.release()
 
@@ -314,7 +347,12 @@ class FSM(DirectObject):
         executing an enterState or exitState function), an
         `AlreadyInTransition` exception is raised (but see `demand()`,
         which will queue these requests up and apply when the
-        transition is complete)."""
+        transition is complete).
+
+        If the previous state's exitFunc or the new state's enterFunc
+        is a coroutine, the state change may not have been applied by
+        the time request() returns, but you can use `await` on the
+        return value to await the transition."""
 
         self.fsmLock.acquire()
         try:
@@ -331,7 +369,7 @@ class FSM(DirectObject):
                     result = (result,) + args
 
                 # Otherwise, assume it's a (name, *args) tuple
-                self.__setState(*result)
+                return self.__setState(*result)
 
             return result
         finally:
@@ -441,11 +479,11 @@ class FSM(DirectObject):
         try:
             if self.stateArray:
                 if not self.state in self.stateArray:
-                    self.request(self.stateArray[0])
+                    return self.request(self.stateArray[0])
                 else:
                     cur_index = self.stateArray.index(self.state)
                     new_index = (cur_index + 1) % len(self.stateArray)
-                    self.request(self.stateArray[new_index], args)
+                    return self.request(self.stateArray[new_index], args)
             else:
                 assert self.notifier.debug(
                                     "stateArray empty. Can't switch to next.")
@@ -459,11 +497,11 @@ class FSM(DirectObject):
         try:
             if self.stateArray:
                 if not self.state in self.stateArray:
-                    self.request(self.stateArray[0])
+                    return self.request(self.stateArray[0])
                 else:
                     cur_index = self.stateArray.index(self.state)
                     new_index = (cur_index - 1) % len(self.stateArray)
-                    self.request(self.stateArray[new_index], args)
+                    return self.request(self.stateArray[new_index], args)
             else:
                 assert self.notifier.debug(
                                     "stateArray empty. Can't switch to next.")
@@ -471,8 +509,26 @@ class FSM(DirectObject):
             self.fsmLock.release()
 
     def __setState(self, newState, *args):
-        # Internal function to change unconditionally to the indicated
-        # state.
+        # Internal function to change unconditionally to the indicated state.
+
+        transition = Transition((newState,) + args)
+
+        # See if we can transition immediately by polling the coroutine.
+        coro = self.__transition(newState, *args)
+        try:
+            coro.send(None)
+        except StopIteration:
+            # We managed to apply this straight away.
+            return transition
+
+        # Continue the state transition in a task.
+        task = PythonTask(coro)
+        mgr = AsyncTaskManager.get_global_ptr()
+        mgr.add(task)
+        transition._future = task
+        return transition
+
+    async def __transition(self, newState, *args):
         assert self.state
         assert self.notify.debug("%s to state %s." % (self._name, newState))
 
@@ -482,8 +538,13 @@ class FSM(DirectObject):
 
         try:
             if not self.__callFromToFunc(self.oldState, self.newState, *args):
-                self.__callExitFunc(self.oldState)
-                self.__callEnterFunc(self.newState, *args)
+                result = self.__callExitFunc(self.oldState)
+                if isinstance(result, types.CoroutineType):
+                    await result
+
+                result = self.__callEnterFunc(self.newState, *args)
+                if isinstance(result, types.CoroutineType):
+                    await result
         except:
             # If we got an exception during the enter or exit methods,
             # go directly to state "InternalError" and raise up the
@@ -503,9 +564,10 @@ class FSM(DirectObject):
         del self.newState
 
         if self.__requestQueue:
-            request = self.__requestQueue.pop(0)
+            request, fut = self.__requestQueue.pop(0)
             assert self.notify.debug("%s continued queued request." % (self._name))
-            request()
+            await request()
+            fut.set_result(None)
 
     def __callEnterFunc(self, name, *args):
         # Calls the appropriate enter function when transitioning into
@@ -517,7 +579,7 @@ class FSM(DirectObject):
             # If there's no matching enterFoo() function, call
             # defaultEnter() instead.
             func = self.defaultEnter
-        func(*args)
+        return func(*args)
 
     def __callFromToFunc(self, oldState, newState, *args):
         # Calls the appropriate fromTo function when transitioning into
@@ -540,7 +602,7 @@ class FSM(DirectObject):
             # If there's no matching exitFoo() function, call
             # defaultExit() instead.
             func = self.defaultExit
-        func()
+        return func()
 
     def __repr__(self):
         return self.__str__()

+ 6 - 1
direct/src/interval/CMakeLists.txt

@@ -37,11 +37,16 @@ set(P3INTERVAL_SOURCES
   waitInterval.cxx
 )
 
+set(P3INTERVAL_IGATEEXT
+  cInterval_ext.cxx
+  cInterval_ext.h
+)
+
 composite_sources(p3interval P3INTERVAL_SOURCES)
 add_component_library(p3interval SYMBOL BUILDING_DIRECT_INTERVAL
   ${P3INTERVAL_HEADERS} ${P3INTERVAL_SOURCES})
 target_link_libraries(p3interval p3directbase panda)
-target_interrogate(p3interval ALL)
+target_interrogate(p3interval ALL EXTENSIONS ${P3INTERVAL_IGATEEXT})
 
 if(NOT BUILD_METALIBS)
   install(TARGETS p3interval

+ 3 - 0
direct/src/interval/cInterval.h

@@ -19,6 +19,7 @@
 #include "pvector.h"
 #include "config_interval.h"
 #include "pStatCollector.h"
+#include "extension.h"
 
 class CIntervalManager;
 
@@ -120,6 +121,8 @@ PUBLISHED:
   bool step_play();
 
 PUBLISHED:
+  EXTENSION(PyObject *__await__(PyObject *self));
+
   MAKE_PROPERTY(name, get_name);
   MAKE_PROPERTY(duration, get_duration);
   MAKE_PROPERTY(open_ended, get_open_ended);

+ 61 - 0
direct/src/interval/cInterval_ext.cxx

@@ -0,0 +1,61 @@
+/**
+ * PANDA 3D SOFTWARE
+ * Copyright (c) Carnegie Mellon University.  All rights reserved.
+ *
+ * All use of this software is subject to the terms of the revised BSD
+ * license.  You should have received a copy of this license along
+ * with this source code in a file named "LICENSE."
+ *
+ * @file cInterval_ext.cxx
+ * @author rdb
+ * @date 2020-10-17
+ */
+
+#include "cInterval_ext.h"
+#include "cIntervalManager.h"
+#include "asyncFuture.h"
+
+#ifdef HAVE_PYTHON
+
+#ifndef CPPPARSER
+extern struct Dtool_PyTypedObject Dtool_CInterval;
+#endif
+
+/**
+ * Yields continuously until the interval is done.
+ */
+static PyObject *gen_next(PyObject *self) {
+  const CInterval *ival;
+  if (!Dtool_Call_ExtractThisPointer(self, Dtool_CInterval, (void **)&ival)) {
+    return nullptr;
+  }
+
+  if (ival->get_state() != CInterval::S_final) {
+    // Try again next frame.
+    Py_INCREF(Py_None);
+    return Py_None;
+  }
+  else {
+    PyErr_SetNone(PyExc_StopIteration);
+    return nullptr;
+  }
+}
+
+/**
+ * Awaiting an interval starts it and yields a future until it is done.
+ */
+PyObject *Extension<CInterval>::
+__await__(PyObject *self) {
+  if (_this->get_state() != CInterval::S_initial) {
+    PyErr_SetString(PyExc_RuntimeError, "Can only await an interval that is in the initial state.");
+    return nullptr;
+  }
+
+  // This may be overridden from Python (such as is the case for Sequence), so
+  // we call this via Python.
+  PyObject *result = PyObject_CallMethod(self, "start", nullptr);
+  Py_XDECREF(result);
+  return Dtool_NewGenerator(self, &gen_next);
+}
+
+#endif  // HAVE_PYTHON

+ 37 - 0
direct/src/interval/cInterval_ext.h

@@ -0,0 +1,37 @@
+/**
+ * PANDA 3D SOFTWARE
+ * Copyright (c) Carnegie Mellon University.  All rights reserved.
+ *
+ * All use of this software is subject to the terms of the revised BSD
+ * license.  You should have received a copy of this license along
+ * with this source code in a file named "LICENSE."
+ *
+ * @file cInterval_ext.h
+ * @author rdb
+ * @date 2020-10-17
+ */
+
+#ifndef CINTERVAL_EXT_H
+#define CINTERVAL_EXT_H
+
+#include "dtoolbase.h"
+
+#ifdef HAVE_PYTHON
+
+#include "extension.h"
+#include "cInterval.h"
+#include "py_panda.h"
+
+/**
+ * This class defines the extension methods for CInterval, which are called
+ * instead of any C++ methods with the same prototype.
+ */
+template<>
+class Extension<CInterval> : public ExtensionBase<CInterval> {
+public:
+  PyObject *__await__(PyObject *self);
+};
+
+#endif  // HAVE_PYTHON
+
+#endif  // CINTERVAL_EXT_H

+ 7 - 40
direct/src/showbase/Loader.py

@@ -31,42 +31,6 @@ class Loader(DirectObject):
         # This indicates that this class behaves like a Future.
         _asyncio_future_blocking = False
 
-        class _ResultAwaiter(object):
-            """Reinvents generators because of PEP 479, sigh.  See #513."""
-
-            __slots__ = 'requestList', 'index'
-
-            def __init__(self, requestList):
-                self.requestList = requestList
-                self.index = 0
-
-            def __await__(self):
-                return self
-
-            def __anext__(self):
-                if self.index >= len(self.requestList):
-                    raise StopAsyncIteration
-                return self
-
-            def __iter__(self):
-                return self
-
-            def __next__(self):
-                i = self.index
-                request = self.requestList[i]
-                if not request.done():
-                    return request
-
-                self.index = i + 1
-
-                result = request.result()
-                if isinstance(result, PandaNode):
-                    result = NodePath(result)
-
-                exc = StopIteration(result)
-                exc.value = result
-                raise exc
-
         def __init__(self, loader, numObjects, gotList, callback, extraArgs):
             self._loader = loader
             self.objects = [None] * numObjects
@@ -124,13 +88,15 @@ class Loader(DirectObject):
 
             if self.requests:
                 self._asyncio_future_blocking = True
+                while self.requests:
+                    yield self
 
             if self.gotList:
-                return self._ResultAwaiter([self])
+                return self.objects
             else:
-                return self._ResultAwaiter(self.requestList)
+                return self.objects[0]
 
-        def __aiter__(self):
+        async def __aiter__(self):
             """ This allows using `async for` to iterate asynchronously over
             the results of this class.  It does guarantee to return the
             results in order, though, even though they may not be loaded in
@@ -138,7 +104,8 @@ class Loader(DirectObject):
             requestList = self.requestList
             assert requestList is not None, "Request was cancelled."
 
-            return self._ResultAwaiter(requestList)
+            for req in requestList:
+                yield await req
 
     # special methods
     def __init__(self, base):

+ 3 - 3
dtool/src/prc/streamWrapper.h

@@ -65,7 +65,7 @@ PUBLISHED:
   ~IStreamWrapper();
 
   INLINE std::istream *get_istream() const;
-  MAKE_PROPERTY(std::istream, get_istream);
+  MAKE_PROPERTY(istream, get_istream);
 
 public:
   void read(char *buffer, std::streamsize num_bytes);
@@ -92,7 +92,7 @@ PUBLISHED:
   ~OStreamWrapper();
 
   INLINE std::ostream *get_ostream() const;
-  MAKE_PROPERTY(std::ostream, get_ostream);
+  MAKE_PROPERTY(ostream, get_ostream);
 
 public:
   void write(const char *buffer, std::streamsize num_bytes);
@@ -128,7 +128,7 @@ PUBLISHED:
   ~StreamWrapper();
 
   INLINE std::iostream *get_iostream() const;
-  MAKE_PROPERTY(std::iostream, get_iostream);
+  MAKE_PROPERTY(iostream, get_iostream);
 
 private:
   std::iostream *_iostream;

+ 6 - 2
makepanda/makepanda.py

@@ -1460,8 +1460,9 @@ def CompileCxx(obj,src,opts):
 
 def CompileBison(wobj, wsrc, opts):
     ifile = os.path.basename(wsrc)
-    wdsth = GetOutputDir()+"/include/" + ifile[:-4] + ".h"
-    wdstc = GetOutputDir()+"/tmp/" + ifile + ".cxx"
+    wdsth = GetOutputDir() + "/include/" + ifile[:-4] + ".h"
+    wdsth2 = GetOutputDir() + "/tmp/" + ifile + ".h"
+    wdstc = GetOutputDir() + "/tmp/" + ifile + ".cxx"
     pre = GetValueOption(opts, "BISONPREFIX_")
     bison = GetBison()
     if bison is None:
@@ -1471,6 +1472,7 @@ def CompileBison(wobj, wsrc, opts):
            os.path.isfile(base + '.cxx.prebuilt'):
             CopyFile(wdstc, base + '.cxx.prebuilt')
             CopyFile(wdsth, base + '.h.prebuilt')
+            CopyFile(wdsth2, base + '.h.prebuilt')
         else:
             exit('Could not find bison!')
     else:
@@ -5087,6 +5089,7 @@ if not PkgSkip("DIRECT"):
     IGATEFILES=GetDirectoryContents('direct/src/interval', ["*.h", "*_composite*.cxx"])
     TargetAdd('libp3interval.in', opts=OPTS, input=IGATEFILES)
     TargetAdd('libp3interval.in', opts=['IMOD:panda3d.direct', 'ILIB:libp3interval', 'SRCDIR:direct/src/interval'])
+    PyTargetAdd('p3interval_cInterval_ext.obj', opts=OPTS, input='cInterval_ext.cxx')
 
 #
 # DIRECTORY: direct/src/showbase/
@@ -5152,6 +5155,7 @@ if not PkgSkip("DIRECT"):
     PyTargetAdd('direct.pyd', input='libp3showbase_igate.obj')
     PyTargetAdd('direct.pyd', input='libp3deadrec_igate.obj')
     PyTargetAdd('direct.pyd', input='libp3interval_igate.obj')
+    PyTargetAdd('direct.pyd', input='p3interval_cInterval_ext.obj')
     if GetTarget() != 'emscripten':
         PyTargetAdd('direct.pyd', input='libp3distributed_igate.obj')
     PyTargetAdd('direct.pyd', input='libp3motiontrail_igate.obj')

+ 1 - 3
panda/src/collide/collisionHandlerGravity.I

@@ -52,8 +52,6 @@ get_reach() const {
  *
  * The object might not necessarily be at rest.  Use is_on_ground() if you
  * want to know whether the object is on the ground and at rest.
- *
- * See Also: is_in_outer_space()
  */
 INLINE PN_stdfloat CollisionHandlerGravity::
 get_airborne_height() const {
@@ -73,7 +71,7 @@ is_on_ground() const {
 
 /**
  * How hard did the object hit the ground.  This value is set on impact with
- * the ground.  You may want to watch (poll) on is_on_groun() and when that is
+ * the ground.  You may want to watch (poll) on is_on_ground() and when that is
  * true, call get_impact_velocity(). Normally I avoid polling, but we are
  * calling is_on_ground() frequently anyway.
  */

+ 4 - 3
panda/src/downloader/httpAuthorization.cxx

@@ -14,6 +14,7 @@
 #include "httpAuthorization.h"
 #include "httpChannel.h"
 #include "urlSpec.h"
+#include "string_utils.h"
 
 #ifdef HAVE_OPENSSL
 
@@ -126,7 +127,7 @@ parse_authentication_schemes(HTTPAuthorization::AuthenticationSchemes &schemes,
       ++q;
     }
     // Here's our first scheme.
-    string scheme = HTTPChannel::downcase(field_value.substr(p, q - p));
+    string scheme = downcase(field_value.substr(p, q - p));
     Tokens *tokens = &(schemes[scheme]);
 
     // Now pull off the tokens, one at a time.
@@ -139,7 +140,7 @@ parse_authentication_schemes(HTTPAuthorization::AuthenticationSchemes &schemes,
       }
       if (field_value[q] == '=') {
         // This is a token.
-        string token = HTTPChannel::downcase(field_value.substr(p, q - p));
+        string token = downcase(field_value.substr(p, q - p));
         string value;
         p = scan_quoted_or_unquoted_string(value, field_value, q + 1);
         (*tokens)[token] = value;
@@ -152,7 +153,7 @@ parse_authentication_schemes(HTTPAuthorization::AuthenticationSchemes &schemes,
 
       } else {
         // This is not a token; it must be the start of a new scheme.
-        scheme = HTTPChannel::downcase(field_value.substr(p, q - p));
+        scheme = downcase(field_value.substr(p, q - p));
         tokens = &(schemes[scheme]);
         p = q + 1;
       }

+ 57 - 25
panda/src/downloader/httpChannel.cxx

@@ -22,6 +22,11 @@
 #include "virtualFileMountHTTP.h"
 #include "ramfile.h"
 #include "globPattern.h"
+#include "string_utils.h"
+
+#ifdef HAVE_ZLIB
+#include "zStream.h"
+#endif
 
 #include <stdio.h>
 
@@ -111,12 +116,14 @@ HTTPChannel(HTTPClient *client) :
   _done_state = S_new;
   _started_download = false;
   _sent_so_far = 0;
+  _body_socket_stream = nullptr;
   _body_stream = nullptr;
   _owns_body_stream = false;
   _sbio = nullptr;
   _cipher_list = _client->get_cipher_list();
   _last_status_code = 0;
   _last_run_time = 0.0f;
+  _download_dest = DD_none;
   _download_to_ramfile = nullptr;
   _download_to_stream = nullptr;
 }
@@ -550,7 +557,7 @@ run() {
  * The user is responsible for passing the returned istream to
  * close_read_body() later.
  */
-ISocketStream *HTTPChannel::
+std::istream *HTTPChannel::
 open_read_body() {
   reset_body_stream();
 
@@ -560,14 +567,14 @@ open_read_body() {
 
   string transfer_coding = downcase(get_header_value("Transfer-Encoding"));
 
-  ISocketStream *result;
+  std::istream *result;
   if (transfer_coding == "chunked") {
     // "chunked" transfer encoding.  This means we will have to decode the
     // length of the file as we read it in chunks.  The IChunkedStream does
     // this.
     _state = S_reading_body;
     _read_index++;
-    result = new IChunkedStream(_source, this);
+    _body_socket_stream = new IChunkedStream(_source, this);
 
   } else {
     // If the transfer encoding is anything else, assume "identity". This is
@@ -576,11 +583,39 @@ open_read_body() {
     // file otherwise.
     _state = S_reading_body;
     _read_index++;
-    result = new IIdentityStream(_source, this, _got_file_size, _file_size);
+    _body_socket_stream = new IIdentityStream(_source, this, _got_file_size, _file_size);
+  }
+  result = _body_socket_stream;
+
+  string content_encoding = trim(get_header_value("Content-Encoding"));
+  if (!content_encoding.empty()) {
+    vector_string content_encodings;
+    tokenize(downcase(content_encoding), content_encodings, ",");
+    for (const string &encoding : content_encodings) {
+      string trimmed = trim(encoding);
+      if (trimmed == "identity") {
+        continue;
+      }
+#ifdef HAVE_ZLIB
+      else if (trimmed == "gzip" || trimmed == "deflate" || trimmed == "x-gzip") {
+        // "deflate" actually includes zlib header, which is accepted as well
+        result = new IDecompressStream(result, true, -1, true);
+      }
+#endif
+      else {
+        downloader_cat.error()
+          << "Content-Encoding not supported: " << trimmed << "\n";
+        delete result;
+        _body_socket_stream = nullptr;
+        _body_stream = nullptr;
+        _owns_body_stream = false;
+        return nullptr;
+      }
+    }
   }
 
-  result->_channel = this;
   _body_stream = result;
+  _body_socket_stream->_channel = this;
   _owns_body_stream = false;
 
   return result;
@@ -785,28 +820,14 @@ get_connection() {
   return stream;
 }
 
-/**
- * Returns the input string with all uppercase letters converted to lowercase.
- */
-string HTTPChannel::
-downcase(const string &s) {
-  string result;
-  result.reserve(s.size());
-  string::const_iterator p;
-  for (p = s.begin(); p != s.end(); ++p) {
-    result += tolower(*p);
-  }
-  return result;
-}
-
 /**
  * Called by ISocketStream destructor when _body_stream is destructing.
  */
 void HTTPChannel::
 body_stream_destructs(ISocketStream *stream) {
-  if (stream == _body_stream) {
+  if (stream == _body_socket_stream) {
     if (_state == S_reading_body) {
-      switch (_body_stream->get_read_state()) {
+      switch (_body_socket_stream->get_read_state()) {
       case ISocketStream::RS_complete:
         finished_body(false);
         break;
@@ -820,6 +841,8 @@ body_stream_destructs(ISocketStream *stream) {
         break;
       }
     }
+
+    _body_socket_stream = nullptr;
     _body_stream = nullptr;
     _owns_body_stream = false;
   }
@@ -2160,7 +2183,7 @@ run_reading_body() {
     std::getline(*_body_stream, line);
   }
 
-  if (!_body_stream->is_closed()) {
+  if (!_body_socket_stream->is_closed()) {
     // There's more to come later.
     return true;
   }
@@ -2281,7 +2304,7 @@ run_download_to_file() {
 
   _download_to_stream->flush();
 
-  if (_body_stream->is_closed()) {
+  if (_body_socket_stream->is_closed()) {
     // Done.
     reset_body_stream();
     close_download_stream();
@@ -2331,7 +2354,7 @@ run_download_to_ram() {
     count = _body_stream->gcount();
   }
 
-  if (_body_stream->is_closed()) {
+  if (_body_socket_stream->is_closed()) {
     // Done.
     reset_body_stream();
     close_download_stream();
@@ -2392,7 +2415,7 @@ run_download_to_stream() {
 
   _download_to_stream->flush();
 
-  if (_body_stream->is_closed()) {
+  if (_body_socket_stream->is_closed()) {
     // Done.
     reset_body_stream();
     close_download_stream();
@@ -3635,6 +3658,14 @@ make_header() {
       << "Content-Length: " << _body.length() << "\r\n";
   }
 
+#ifdef HAVE_ZLIB
+  stream
+    << "Accept-Encoding: gzip, deflate, identity\r\n";
+#else
+  stream
+    << "Accept-Encoding: identity\r\n";
+#endif
+
   _header = stream.str();
 }
 
@@ -3810,6 +3841,7 @@ reset_body_stream() {
       nassertv(_body_stream == nullptr && !_owns_body_stream);
     }
   } else {
+    _body_socket_stream = nullptr;
     _body_stream = nullptr;
   }
 }

+ 3 - 4
panda/src/downloader/httpChannel.h

@@ -182,7 +182,7 @@ PUBLISHED:
   bool run();
   INLINE void begin_connect_to(const DocumentSpec &url);
 
-  ISocketStream *open_read_body();
+  std::istream *open_read_body();
   void close_read_body(std::istream *stream) const;
 
   BLOCKING bool download_to_file(const Filename &filename, bool subdocument_resumes = true);
@@ -195,7 +195,6 @@ PUBLISHED:
   INLINE bool is_download_complete() const;
 
 public:
-  static std::string downcase(const std::string &s);
   void body_stream_destructs(ISocketStream *stream);
 
 private:
@@ -336,7 +335,6 @@ private:
   DocumentSpec _document_spec;
   DocumentSpec _request;
   HTTPEnum::Method _method;
-  std::string request_path;
   std::string _header;
   std::string _body;
   std::string _content_type;
@@ -417,7 +415,8 @@ private:
   size_t _sent_so_far;
   std::string _current_field_name;
   std::string _current_field_value;
-  ISocketStream *_body_stream;
+  ISocketStream *_body_socket_stream;
+  std::istream *_body_stream;
   bool _owns_body_stream;
   BIO *_sbio;
   std::string _cipher_list;

+ 6 - 47
panda/src/downloader/httpClient.cxx

@@ -21,6 +21,7 @@
 #include "httpBasicAuthorization.h"
 #include "httpDigestAuthorization.h"
 #include "globPattern.h"
+#include "string_utils.h"
 
 #ifdef HAVE_OPENSSL
 
@@ -30,48 +31,6 @@ using std::string;
 
 PT(HTTPClient) HTTPClient::_global_ptr;
 
-/**
- *
- */
-static string
-trim_blanks(const string &str) {
-  size_t start = 0;
-  while (start < str.length() && isspace(str[start])) {
-    start++;
-  }
-
-  size_t end = str.length();
-  while (end > start && isspace(str[end - 1])) {
-    end--;
-  }
-
-  return str.substr(start, end - start);
-}
-
-/**
- * Chops the source string up into pieces delimited by any of the characters
- * specified in delimiters.  Repeated delimiter characters represent zero-
- * length tokens.
- *
- * It is the user's responsibility to ensure the output vector is cleared
- * before calling this function; the results will simply be appended to the
- * end of the vector.
- */
-static void
-tokenize(const string &str, vector_string &words, const string &delimiters) {
-  size_t p = 0;
-  while (p < str.length()) {
-    size_t q = str.find_first_of(delimiters, p);
-    if (q == string::npos) {
-      words.push_back(str.substr(p));
-      return;
-    }
-    words.push_back(str.substr(p, q - p));
-    p = q + 1;
-  }
-  words.push_back(string());
-}
-
 #ifndef NDEBUG
 /**
  * This method is attached as a callback for SSL messages only when debug
@@ -341,7 +300,7 @@ void HTTPClient::
 set_proxy_spec(const string &proxy_spec) {
   clear_proxy();
 
-  string trim_proxy_spec = trim_blanks(proxy_spec);
+  string trim_proxy_spec = trim(proxy_spec);
 
   // Tokenize the string based on the semicolons.
   if (!trim_proxy_spec.empty()) {
@@ -359,10 +318,10 @@ set_proxy_spec(const string &proxy_spec) {
       size_t equals = spec.find('=');
       if (equals == string::npos) {
         scheme = "";
-        proxy = trim_blanks(spec);
+        proxy = trim(spec);
       } else {
-        scheme = trim_blanks(spec.substr(0, equals));
-        proxy = trim_blanks(spec.substr(equals + 1));
+        scheme = trim(spec.substr(0, equals));
+        proxy = trim(spec.substr(equals + 1));
       }
 
       if (proxy == "DIRECT" || proxy.empty()) {
@@ -426,7 +385,7 @@ set_direct_host_spec(const string &direct_host_spec) {
   for (vector_string::const_iterator hi = hosts.begin();
        hi != hosts.end();
        ++hi) {
-    string spec = trim_blanks(*hi);
+    string spec = trim(*hi);
 
     // We should be careful to avoid adding any empty hostnames to the list.
     // In particular, we will get one empty hostname if the direct_host_spec

+ 18 - 10
panda/src/downloader/httpCookie.I

@@ -11,15 +11,6 @@
  * @date 2004-08-26
  */
 
-/**
- * Constructs an empty cookie.
- */
-INLINE HTTPCookie::
-HTTPCookie() :
-  _secure(false)
-{
-}
-
 /**
  * Constructs a cookie according to the indicated string, presumably the tag
  * of a Set-Cookie header.  There is no way to detect a formatting error in
@@ -40,7 +31,8 @@ HTTPCookie(const std::string &name, const std::string &path, const std::string &
   _name(name),
   _path(path),
   _domain(domain),
-  _secure(false)
+  _secure(false),
+  _samesite(SS_unspecified)
 {
 }
 
@@ -168,6 +160,22 @@ get_secure() const {
   return _secure;
 }
 
+/**
+ *
+ */
+INLINE void HTTPCookie::
+set_samesite(SameSite samesite) {
+  _samesite = samesite;
+}
+
+/**
+ *
+ */
+INLINE HTTPCookie::SameSite HTTPCookie::
+get_samesite() const {
+  return _samesite;
+}
+
 /**
  * Returns true if the cookie's expiration date is before the indicated date,
  * false otherwise.

+ 39 - 3
panda/src/downloader/httpCookie.cxx

@@ -16,6 +16,7 @@
 #ifdef HAVE_OPENSSL
 
 #include "httpChannel.h"
+#include "string_utils.h"
 
 #include <ctype.h>
 
@@ -59,6 +60,7 @@ update_from(const HTTPCookie &other) {
   _value = other._value;
   _expires = other._expires;
   _secure = other._secure;
+  _samesite = other._samesite;
 }
 
 /**
@@ -74,6 +76,7 @@ parse_set_cookie(const string &format, const URLSpec &url) {
   _path = url.get_path();
   _expires = HTTPDate();
   _secure = false;
+  _samesite = SS_unspecified;
 
   bool okflag = true;
   bool first_param = true;
@@ -147,12 +150,16 @@ output(std::ostream &out) const {
       << "; path=" << _path << "; domain=" << _domain;
 
   if (has_expires()) {
-    out << "; expires=" << _expires;
+    out << "; expires=" << _expires.get_string();
   }
 
   if (_secure) {
     out << "; secure";
   }
+
+  if (_samesite != SS_unspecified) {
+    out << "; samesite=" << _samesite;
+  }
 }
 
 /**
@@ -178,7 +185,7 @@ parse_cookie_param(const string &param, bool first_param) {
     _value = value;
 
   } else {
-    key = HTTPChannel::downcase(key);
+    key = downcase(key);
     if (key == "expires") {
       _expires = HTTPDate(value);
       if (!_expires.is_valid()) {
@@ -189,7 +196,7 @@ parse_cookie_param(const string &param, bool first_param) {
       _path = value;
 
     } else if (key == "domain") {
-      _domain = HTTPChannel::downcase(value);
+      _domain = downcase(value);
 
       // From RFC 2965: If an explicitly specified value does not start with a
       // dot, the user agent supplies a leading dot.
@@ -200,6 +207,18 @@ parse_cookie_param(const string &param, bool first_param) {
     } else if (key == "secure") {
       _secure = true;
 
+    } else if (key == "samesite") {
+      value = downcase(value);
+      if (value == "lax") {
+        _samesite = SS_lax;
+      }
+      else if (value == "strict") {
+        _samesite = SS_strict;
+      }
+      else if (value == "none") {
+        _samesite = SS_none;
+      }
+
     } else {
       return false;
     }
@@ -208,4 +227,21 @@ parse_cookie_param(const string &param, bool first_param) {
   return true;
 }
 
+std::ostream &operator << (std::ostream &out, HTTPCookie::SameSite samesite) {
+  switch (samesite) {
+  case HTTPCookie::SS_unspecified:
+    return out;
+
+  case HTTPCookie::SS_lax:
+    return out << "lax";
+
+  case HTTPCookie::SS_strict:
+    return out << "strict";
+
+  case HTTPCookie::SS_none:
+    return out << "none";
+  }
+  return out;
+}
+
 #endif  // HAVE_OPENSSL

+ 22 - 2
panda/src/downloader/httpCookie.h

@@ -31,7 +31,7 @@
  */
 class EXPCL_PANDA_DOWNLOADER HTTPCookie {
 PUBLISHED:
-  INLINE HTTPCookie();
+  INLINE HTTPCookie() = default;
   INLINE explicit HTTPCookie(const std::string &format, const URLSpec &url);
   INLINE explicit HTTPCookie(const std::string &name, const std::string &path,
                              const std::string &domain);
@@ -57,6 +57,16 @@ PUBLISHED:
   INLINE void set_secure(bool flag);
   INLINE bool get_secure() const;
 
+  enum SameSite {
+    SS_unspecified,
+    SS_lax,
+    SS_strict,
+    SS_none,
+  };
+
+  INLINE void set_samesite(SameSite samesite);
+  INLINE SameSite get_samesite() const;
+
   bool operator < (const HTTPCookie &other) const;
   void update_from(const HTTPCookie &other);
 
@@ -66,6 +76,14 @@ PUBLISHED:
 
   void output(std::ostream &out) const;
 
+PUBLISHED:
+  MAKE_PROPERTY(name, get_name, set_name);
+  MAKE_PROPERTY(value, get_value, set_value);
+  MAKE_PROPERTY(domain, get_domain, set_domain);
+  MAKE_PROPERTY(path, get_path, set_path);
+  MAKE_PROPERTY2(expires, has_expires, get_expires, set_expires, clear_expires);
+  MAKE_PROPERTY(secure, get_secure, set_secure);
+
 private:
   bool parse_cookie_param(const std::string &param, bool first_param);
 
@@ -74,9 +92,11 @@ private:
   std::string _path;
   std::string _domain;
   HTTPDate _expires;
-  bool _secure;
+  bool _secure = false;
+  SameSite _samesite = SS_unspecified;
 };
 
+std::ostream &operator << (std::ostream &out, HTTPCookie::SameSite samesite);
 INLINE std::ostream &operator << (std::ostream &out, const HTTPCookie &cookie);
 
 #include "httpCookie.I"

+ 3 - 2
panda/src/downloader/httpDigestAuthorization.cxx

@@ -16,6 +16,7 @@
 #ifdef HAVE_OPENSSL
 
 #include "httpChannel.h"
+#include "string_utils.h"
 #include "openSSLWrapper.h"  // must be included before any other openssl.
 #include <openssl/ssl.h>
 #include <openssl/md5.h>
@@ -50,7 +51,7 @@ HTTPDigestAuthorization(const HTTPAuthorization::Tokens &tokens,
   _algorithm = A_md5;
   ti = tokens.find("algorithm");
   if (ti != tokens.end()) {
-    string algo_str = HTTPChannel::downcase((*ti).second);
+    string algo_str = downcase((*ti).second);
     if (algo_str == "md5") {
       _algorithm = A_md5;
     } else if (algo_str == "md5-sess") {
@@ -63,7 +64,7 @@ HTTPDigestAuthorization(const HTTPAuthorization::Tokens &tokens,
   _qop = 0;
   ti = tokens.find("qop");
   if (ti != tokens.end()) {
-    string qop_str = HTTPChannel::downcase((*ti).second);
+    string qop_str = downcase((*ti).second);
     // A comma-delimited list of tokens.
 
     size_t p = 0;

+ 20 - 0
panda/src/downloader/virtualFileHTTP.cxx

@@ -140,6 +140,26 @@ open_read_file(bool auto_unwrap) const {
   return return_file(strstream, auto_unwrap);
 }
 
+/**
+ * Fills up the indicated pvector with the contents of the file, if it is a
+ * regular file.  Returns true on success, false otherwise.
+ */
+bool VirtualFileHTTP::
+read_file(vector_uchar &result, bool auto_unwrap) const {
+  if (_status_only) {
+    return false;
+  }
+
+  Ramfile ramfile;
+  if (!_channel->download_to_ram(&ramfile, false)) {
+    return false;
+  }
+
+  const string &data = ramfile.get_data();
+  std::copy(data.begin(), data.end(), std::back_inserter(result));
+  return true;
+}
+
 /**
  * Downloads the entire file from the web server into the indicated iostream.
  * Returns true on success, false on failure.

+ 2 - 0
panda/src/downloader/virtualFileHTTP.h

@@ -51,6 +51,8 @@ public:
   virtual std::streamsize get_file_size() const;
   virtual time_t get_timestamp() const;
 
+  virtual bool read_file(vector_uchar &result, bool auto_unwrap) const;
+
 private:
   bool fetch_file(std::ostream *buffer_stream) const;
   std::istream *return_file(std::istream *buffer_stream, bool auto_unwrap) const;

+ 5 - 0
panda/src/event/pythonTask.cxx

@@ -624,6 +624,11 @@ do_python_task() {
         return DS_done;
       }
 
+    } else if (result == Py_None) {
+      // Bare yield means to continue next frame.
+      Py_DECREF(result);
+      return DS_cont;
+
     } else if (DtoolInstance_Check(result)) {
       // We are waiting for an AsyncFuture (eg. other task) to finish.
       AsyncFuture *fut = (AsyncFuture *)DtoolInstance_UPCAST(result, Dtool_AsyncFuture);

+ 1 - 1
panda/src/linmath/lmatrix4_src.I

@@ -1021,7 +1021,7 @@ operator += (const FLOATNAME(LMatrix4) &other) {
 }
 
 /**
- * Performs a memberwise addition between two matrices.
+ * Performs a memberwise subtraction between two matrices.
  */
 INLINE_LINMATH FLOATNAME(LMatrix4) &FLOATNAME(LMatrix4)::
 operator -= (const FLOATNAME(LMatrix4) &other) {

+ 2 - 2
samples/roaming-ralph/main.py

@@ -268,7 +268,7 @@ class RoamingRalphDemo(ShowBase):
         entries.sort(key=lambda x: x.getSurfacePoint(render).getZ())
 
         for entry in entries:
-            if entry.getIntoNode().getName() == "terrain":
+            if entry.getIntoNode().name == "terrain":
                 self.ralph.setZ(entry.getSurfacePoint(render).getZ())
 
         # Keep the camera at one unit above the terrain,
@@ -278,7 +278,7 @@ class RoamingRalphDemo(ShowBase):
         entries.sort(key=lambda x: x.getSurfacePoint(render).getZ())
 
         for entry in entries:
-            if entry.getIntoNode().getName() == "terrain":
+            if entry.getIntoNode().name == "terrain":
                 self.camera.setZ(entry.getSurfacePoint(render).getZ() + 1.5)
         if self.camera.getZ() < self.ralph.getZ() + 2.0:
             self.camera.setZ(self.ralph.getZ() + 2.0)